dsh-prime-memory 0.26.6 → 0.26.9
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/README.md +8 -1
- package/dist/client.js +45 -5
- package/dist/config.d.ts +31 -0
- package/dist/config.js +25 -3
- package/dist/contract.d.ts +12 -1
- package/dist/index.js +6 -0
- package/dist/llm.d.ts +14 -0
- package/dist/llm.js +42 -3
- package/dist/pipeline/l1.d.ts +15 -2
- package/dist/pipeline/l1.js +76 -23
- package/dist/pipeline/l2.js +72 -50
- package/dist/pipeline/ruminate.js +2 -0
- package/dist/pipeline/runner.d.ts +6 -0
- package/dist/pipeline/runner.js +35 -4
- package/dist/settings.js +3 -0
- package/dist/stats.js +18 -3
- package/dist/store/file-versions.d.ts +2 -0
- package/dist/store/file-versions.js +2 -0
- package/dist/store/route-health.d.ts +38 -0
- package/dist/store/route-health.js +115 -0
- package/dsh.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -352,7 +352,7 @@ 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` | `
|
|
355
|
+
| `llm.maxInputChars` | `200000` | 单次蒸馏调用输入闸门(超限截断);默认 200k 适配本地/免费模型窗口,上限 256k,可再往下调。L1 抽取/L2 另有 24k 分块硬顶,不受此值影响;运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
|
|
356
356
|
| `llm.timeoutMs` | `120000` | 单次蒸馏调用超时(ms) |
|
|
357
357
|
| `tokenCost.retentionDays` | `365` | 蒸馏成本明细(token\_cost 表)保留天数,写入时滚动清理更早行;`0` = 永久保留。成本看板「近 N 天」窗口上限同此值 |
|
|
358
358
|
| `tools` | `true` | 是否注册模型可调用的记忆工具 |
|
|
@@ -415,6 +415,13 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
415
415
|
|
|
416
416
|
4. **调高超时**:`llm.timeoutMs` 只在路由确实慢但网关不掐时有用;网关 20s 掐断的场景调插件超时无效,请用前两层。
|
|
417
417
|
|
|
418
|
+
### 二线大容量兜底与超时可调(v0.26.7+)
|
|
419
|
+
|
|
420
|
+
- **面板可调超时**:设置 → 记忆 → 蒸馏参数 → **蒸馏超时(ms)**(`distillTimeoutMs`,即时生效、无需重启)。只救"慢但未被网关掐"的路由;供应商网关侧固定掐断(如某些免费路由 60s)抬此值无效,须换路由。
|
|
421
|
+
- **二线模型(大容量兜底)**:`llm.secondary{provider,model,triggerChars}`——单次调用输入 ≥ `triggerChars` 才走二线(小调用走便宜快的一线、肥调用走能吞大输入的二线)。未配则不启用、绝不静默改选任意模型。
|
|
422
|
+
- **分块分载与容错**:L1 抽取 / L2 场景整合按字符预算分块,单块超时/崩只丢该块、留待重试,不再"一块崩整批丢"(待重试不再被整片回滚)。`llm.extractTimeBudgetMs`(默认 0=不限)可给整轮抽取设墙钟预算,防一个慢模型独占后台;`llm.modelMaxInputChars` 声明模型真实输入上限,块大小据此收紧。
|
|
423
|
+
- **显式路由(灰度)**:`llm.requireExplicitRoute=true` 时,未显式配一线/二线/回退链 → 蒸馏顺延并告警,不再蹭宿主默认模型(默认 false 保持旧行为)。
|
|
424
|
+
|
|
418
425
|
## 日志与故障排查
|
|
419
426
|
|
|
420
427
|
dsh 宿主把插件日志打到控制台;插件另把 info 级以上镜像到数据目录的 `memory.log`。
|
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" ?
|
|
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~
|
|
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(
|
|
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
|
-
//
|
|
365
|
-
|
|
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]),
|
package/dist/contract.d.ts
CHANGED
|
@@ -77,7 +77,10 @@ export interface MemoryLiveSettings {
|
|
|
77
77
|
/** 输入预算运行时覆盖(字符,≈token):单次蒸馏调用的输入上限,L1 按此分块、
|
|
78
78
|
* 超限截断;0 = 跟随静态配置 llm.maxInputChars。 */
|
|
79
79
|
distillMaxInputChars: number;
|
|
80
|
-
/**
|
|
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
|
-
|
|
374
|
-
|
|
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 =
|
|
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
|
}
|
package/dist/pipeline/l1.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
88
|
+
mergeRekey?: (oldId: string, newId: string) => void,
|
|
89
|
+
/** R9 路由健康学习(可选):读已学到的跑通块长收紧 perChunk,成功块回记。未传=不启用。 */
|
|
90
|
+
routeHealth?: RouteHealthStore): Promise<ExtractionResult>;
|
package/dist/pipeline/l1.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import { normWingEnabled } from '../config.js';
|
|
11
|
-
import { callLLM, parseJsonLogged, resolveLayerTokens } from '../llm.js';
|
|
11
|
+
import { callLLM, parseJsonLogged, resolveLayerTokens, resolveModelRoute } from '../llm.js';
|
|
12
12
|
import { buildReceipts, newRunId, persistReceiptsSafely } from '../store/receipts.js';
|
|
13
13
|
import { buildConflictPair, conflictRejectId, DEFER_MAX, normalizeClaimKey, normalizeConflictType, occupiesConflictQuota, pendingHardTotal, validateConflictPair, } from '../store/conflicts.js';
|
|
14
14
|
import { formatExtractionPrompt, getExtractMemoriesSystemPrompt } from '../prompts/l1-extraction.js';
|
|
@@ -23,13 +23,19 @@ import { routeWorkspace } from '../workspace-route.js';
|
|
|
23
23
|
/**
|
|
24
24
|
* L1 抽取单块字符硬顶(修复 #10 分批过大)
|
|
25
25
|
*
|
|
26
|
-
* `cfg.llm.maxInputChars` 的语义是「单次 LLM 调用输入闸门」(默认
|
|
26
|
+
* `cfg.llm.maxInputChars` 的语义是「单次 LLM 调用输入闸门」(默认 200k),被复用为
|
|
27
27
|
* 分块预算时算出 658k/块——一次输出须在同一 JSON 数组写完全部抽出记忆,块越大字符串越多,
|
|
28
28
|
* 任一处长中文里未转义 ASCII `"` 即整批 parse 失败、该块记忆全丢;慢模型更直接撞 120s 超时。
|
|
29
29
|
* 砍到 24k 后输出 ≈2–4k tokens,仍远低于 LAYER_MAX_TOKENS_EXTRACT(16k),不会引入截断,
|
|
30
30
|
* 单块失败半径从「几百条」缩到「几十条」。代价是多几次小调用,但每次 5–15s 完成。
|
|
31
31
|
*/
|
|
32
32
|
const MAX_EXTRACT_CHUNK_CHARS = 24_000;
|
|
33
|
+
/**
|
|
34
|
+
* L2 场景整合单块字符硬顶(R1):与抽取同精神——旧版一次把整族 backlog 塞单调用,
|
|
35
|
+
* 库越大越必撞超时(实测 work-L2 >60s abort)。砍到 24k 后逐块整合,失败半径从
|
|
36
|
+
* “整族 backlog”缩到“一块”。默认对齐 MAX_EXTRACT_CHUNK_CHARS。
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_SCENE_CHUNK_CHARS = 24_000;
|
|
33
39
|
/**
|
|
34
40
|
* L1 去重单批发起上限(与抽取同精神)
|
|
35
41
|
*
|
|
@@ -266,7 +272,9 @@ anchorMap,
|
|
|
266
272
|
* 另两个 sidecar 里,而 merge 每次新铸 id——不搬就等于每次合并把这条记忆的
|
|
267
273
|
* `used`/人工打分清零,它会被当成“没用过”而沉底。缺省不搬(行为与改动前一致)。
|
|
268
274
|
*/
|
|
269
|
-
mergeRekey
|
|
275
|
+
mergeRekey,
|
|
276
|
+
/** R9 路由健康学习(可选):读已学到的跑通块长收紧 perChunk,成功块回记。未传=不启用。 */
|
|
277
|
+
routeHealth) {
|
|
270
278
|
if (!cfg.extract.enabled)
|
|
271
279
|
return { stored: 0, skipped: true, sceneName: chainHead(states, mode), newRecords: [] };
|
|
272
280
|
// 触发阈值(渐进爬坡 + 按会话切片计数)由 runner 判定(trigger.ts);
|
|
@@ -314,12 +322,31 @@ mergeRekey) {
|
|
|
314
322
|
const backgroundMsgs = background.slice(-cfg.extract.backgroundMessages);
|
|
315
323
|
// 预留:背景消息(≤10 条 ×4000 字)+ prompt 脚手架
|
|
316
324
|
// 硬顶 24k(见 MAX_EXTRACT_CHUNK_CHARS 注释);若配置更小则跟随配置
|
|
317
|
-
|
|
325
|
+
let perChunk = Math.min(MAX_EXTRACT_CHUNK_CHARS, Math.max(20_000, cfg.llm.maxInputChars - 42_000));
|
|
326
|
+
// R5: 尊重模型真实输入上限(若声明),避免 700k 盲值撞本地模型硬限
|
|
327
|
+
if (cfg.llm.modelMaxInputChars && cfg.llm.modelMaxInputChars > 0) {
|
|
328
|
+
perChunk = Math.min(perChunk, Math.max(20_000, cfg.llm.modelMaxInputChars - 42_000));
|
|
329
|
+
}
|
|
330
|
+
// R9: 用该路由已学到的跑通块长再收紧 perChunk(best-effort;路由解析失败则跳过)
|
|
331
|
+
let rhKey;
|
|
332
|
+
if (routeHealth) {
|
|
333
|
+
try {
|
|
334
|
+
rhKey = await resolveModelRoute(ctx, cfg);
|
|
335
|
+
const lg = routeHealth.lastGoodChunkChars(rhKey.provider, rhKey.model);
|
|
336
|
+
if (lg > 0)
|
|
337
|
+
perChunk = Math.min(perChunk, Math.max(20_000, lg));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
rhKey = undefined;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
318
343
|
const chunks = chunkByCharBudget(pending, perChunk);
|
|
319
344
|
if (chunks.length > 1) {
|
|
320
345
|
logger.info(`[memory] L1 输入超预算(${pending.length} 条消息),分 ${chunks.length} 块抽取(每块 ≤${perChunk} 字符)`);
|
|
321
346
|
}
|
|
322
347
|
const extracted = [];
|
|
348
|
+
// R1′ 抽取块级失败的消息(留桶下轮重投,实现部分消费)
|
|
349
|
+
const failedMessages = [];
|
|
323
350
|
let lastScene = chainState.lastSceneName;
|
|
324
351
|
let sceneCount = 0;
|
|
325
352
|
// 治理归属(ADR-0015 T1.7/T1.9):repoKey=basename(归一 cwd),**进管线时一次算定**
|
|
@@ -330,30 +357,52 @@ mergeRekey) {
|
|
|
330
357
|
const halls = mode === 'auto' && !hallCandidates.includes(WING_FALLBACK)
|
|
331
358
|
? [...hallCandidates, WING_FALLBACK]
|
|
332
359
|
: hallCandidates;
|
|
360
|
+
const timeBudgetMs = cfg.llm.extractTimeBudgetMs ?? 0;
|
|
361
|
+
const extractStart = Date.now();
|
|
333
362
|
for (const chunk of chunks) {
|
|
363
|
+
// R5 整轮墙钟预算:超预算把本块(及后续块)留 pending 下轮续,防慢模型独占串行泵
|
|
364
|
+
if (timeBudgetMs > 0 && chunks.length > 1 && Date.now() - extractStart > timeBudgetMs) {
|
|
365
|
+
failedMessages.push(...chunk);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
334
368
|
const userPrompt = formatExtractionPrompt({
|
|
335
369
|
newMessages: chunk,
|
|
336
370
|
backgroundMessages: backgroundMsgs,
|
|
337
371
|
previousSceneName: lastScene || '无',
|
|
338
372
|
halls,
|
|
339
373
|
});
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
374
|
+
let scenes;
|
|
375
|
+
try {
|
|
376
|
+
const raw = await callLLM(ctx, cfg, {
|
|
377
|
+
system: getExtractMemoriesSystemPrompt(mode, {
|
|
378
|
+
usageScenario: cfg.usageScenario?.enabled === true,
|
|
379
|
+
narrative: cfg.narrativeTemplates?.enabled === true,
|
|
380
|
+
// 名称蒸馏(Phase 3):示例段只在总开关开着时付(它与 title 是两层成本,
|
|
381
|
+
// 分开开关才能 A/B)。关着时这份 opts 产出的提示与改动前逐字相同。
|
|
382
|
+
title: cfg.naming?.enabled === true,
|
|
383
|
+
fewShot: cfg.naming?.enabled === true && cfg.naming.fewShot === true,
|
|
384
|
+
}),
|
|
385
|
+
user: userPrompt,
|
|
386
|
+
maxTokens: resolveLayerTokens(cfg, 'extract'),
|
|
387
|
+
layer: 'l1-extract',
|
|
388
|
+
logger,
|
|
389
|
+
});
|
|
390
|
+
const parsed = parseJsonLogged(raw, 'L1 抽取', logger);
|
|
391
|
+
if (!Array.isArray(parsed))
|
|
392
|
+
throw new Error('L1 抽取输出不是 JSON 数组');
|
|
393
|
+
scenes = parsed;
|
|
394
|
+
// R9: 记录该路由跑通的实际块长(只降不升),供下轮收紧 perChunk
|
|
395
|
+
if (routeHealth && rhKey) {
|
|
396
|
+
const chunkChars = chunk.reduce((n, m) => n + m.content.length + 64, 0);
|
|
397
|
+
routeHealth.recordGood(rhKey.provider, rhKey.model, chunkChars);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
// R1′ 块级容错:单块超时/崩只丢这块、留桶下轮重投,不掀翻整批(旧行为是整抛)
|
|
402
|
+
failedMessages.push(...chunk);
|
|
403
|
+
logger.warn(`[memory] L1 抽取块失败(${chunk.length} 条消息留待重投): ${err instanceof Error ? err.message : String(err)}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
357
406
|
for (const scene of scenes) {
|
|
358
407
|
if (!scene || typeof scene.scene_name !== 'string')
|
|
359
408
|
continue;
|
|
@@ -397,10 +446,14 @@ mergeRekey) {
|
|
|
397
446
|
}
|
|
398
447
|
}
|
|
399
448
|
if (extracted.length === 0) {
|
|
449
|
+
// 全部块失败:等价旧"整抛"语义——runner catch 会保留整片 + 记退避(不误当零产出成功)
|
|
450
|
+
if (failedMessages.length > 0) {
|
|
451
|
+
throw new Error(`L1 抽取全部 ${chunks.length} 块失败(逐块错误见上)`);
|
|
452
|
+
}
|
|
400
453
|
logger.info(`[memory] L1 抽取完成:无可提取记忆(mode=${mode},${pending.length} 条消息,${sceneCount} 个情境)`);
|
|
401
454
|
// 成功但零产出:同样推进时间戳/场景,与失败(lastExtractAt 保持 0)区分开
|
|
402
455
|
markExtracted(states, mode, lastScene);
|
|
403
|
-
return { stored: 0, skipped: false, sceneName: lastScene, newRecords: [] };
|
|
456
|
+
return { stored: 0, skipped: false, sceneName: lastScene, newRecords: [], failedMessages: [] };
|
|
404
457
|
}
|
|
405
458
|
// ── Step 2: 去重(batch 冲突检测;候选只在本族内召回,去重永不跨族) ──
|
|
406
459
|
const matches = await Promise.all(extracted.map(async (m) => ({
|
|
@@ -773,7 +826,7 @@ mergeRekey) {
|
|
|
773
826
|
}
|
|
774
827
|
markExtracted(states, mode, lastScene);
|
|
775
828
|
logger.info(`[memory] L1 抽取完成(mode=${mode}):消息 ${pending.length} 条,抽取 ${extracted.length} 条,去重后新增 ${added.length} 条(取代退场 ${supersededBy.size} 条,chat=${addedByFamily.chat}/work=${addedByFamily.work}),累计 chat=${states.chat.totalExtracted}/work=${states.work.totalExtracted}`);
|
|
776
|
-
return { stored: added.length, skipped: false, sceneName: lastScene, newRecords: added, runId, byKind };
|
|
829
|
+
return { stored: added.length, skipped: false, sceneName: lastScene, newRecords: added, runId, byKind, failedMessages };
|
|
777
830
|
}
|
|
778
831
|
/** auto 档取最近活跃的族 checkpoint(情境链/计数锚点)。 */
|
|
779
832
|
function activeState(states) {
|
package/dist/pipeline/l2.js
CHANGED
|
@@ -1,71 +1,93 @@
|
|
|
1
1
|
import { callLLM, parseJsonLogged, resolveLayerTokens } from '../llm.js';
|
|
2
2
|
import { buildScenePrompt, formatSceneSummaries } from '../prompts/scene.js';
|
|
3
3
|
import { Bm25Index } from '../store/bm25.js';
|
|
4
|
+
import { chunkByCharBudget, MAX_SCENE_CHUNK_CHARS } from './l1.js';
|
|
4
5
|
const REQUEST_RE = /\[PERSONA_UPDATE_REQUEST\]\s*reason:\s*([\s\S]*?)\[\/PERSONA_UPDATE_REQUEST\]/;
|
|
5
6
|
export async function runSceneConsolidation(ctx, cfg, scenes, newMemories, logger, family) {
|
|
6
7
|
if (!cfg.l2.enabled || newMemories.length === 0) {
|
|
7
8
|
return { changed: 0 };
|
|
8
9
|
}
|
|
9
10
|
const summaries = await scenes.list();
|
|
10
|
-
//
|
|
11
|
-
const memoriesJson = JSON.stringify(newMemories.map((m) => ({
|
|
12
|
-
record_id: m.id,
|
|
13
|
-
content: m.content,
|
|
14
|
-
type: m.type,
|
|
15
|
-
priority: m.priority,
|
|
16
|
-
scene_name: m.scene_name,
|
|
17
|
-
// repo 归属(ADR-0015 T1.9/T1.10):L2 场景归属决策可见项目边界——
|
|
18
|
-
// this-repo 记录不得并入跨项目场景;'' = 未归属(不引导分组)。
|
|
19
|
-
repoKeyName: m.repoKeyName ?? '',
|
|
20
|
-
applicability: m.applicability ?? '',
|
|
21
|
-
timestamps: m.timestamps.map((t) => new Date(t).toISOString()),
|
|
22
|
-
})), null, 2);
|
|
11
|
+
// 跨块共享的上下文:相关场景全文只选一次(BM25 按全量新记忆选),各块复用。
|
|
23
12
|
const sceneSummaries = formatSceneSummaries(summaries);
|
|
24
13
|
const sceneContents = await pickSceneContents(scenes, summaries, newMemories, cfg.l2.sceneContextLimit);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
family,
|
|
33
|
-
});
|
|
34
|
-
const raw = await callLLM(ctx, cfg, {
|
|
35
|
-
system: systemPrompt,
|
|
36
|
-
user: userPrompt,
|
|
37
|
-
maxTokens: resolveLayerTokens(cfg, 'l2'),
|
|
38
|
-
layer: 'l2',
|
|
39
|
-
logger,
|
|
40
|
-
});
|
|
41
|
-
// ── 解析:先取 PERSONA_UPDATE_REQUEST 标记,再解析操作数组 ──
|
|
42
|
-
const reqMatch = REQUEST_RE.exec(raw);
|
|
43
|
-
const personaRequestedReason = reqMatch?.[1]?.trim() || undefined;
|
|
44
|
-
const opsRaw = reqMatch ? raw.replace(REQUEST_RE, '') : raw;
|
|
45
|
-
const ops = parseJsonLogged(opsRaw, 'L2 场景操作', logger);
|
|
46
|
-
if (!Array.isArray(ops))
|
|
47
|
-
throw new Error('L2 输出不是操作数组');
|
|
14
|
+
// R1 分块分载:把 newMemories 按字符预算切块逐块整合(旧版一次塞整族 backlog → 大库必撞超时)。
|
|
15
|
+
// 单块(小 backlog)时与改前逐字一致(零漂移);多块时跨块合并判断为近似(诚实降级)。
|
|
16
|
+
const perChunk = Math.min(MAX_SCENE_CHUNK_CHARS, Math.max(20_000, cfg.llm.maxInputChars - 42_000));
|
|
17
|
+
const chunks = chunkByCharBudget(newMemories, perChunk);
|
|
18
|
+
if (chunks.length > 1) {
|
|
19
|
+
logger.info(`[memory] L2 输入超预算(${newMemories.length} 条新记忆),分 ${chunks.length} 块整合(每块 ≤${perChunk} 字符)`);
|
|
20
|
+
}
|
|
48
21
|
let changed = 0;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
22
|
+
let failedChunks = 0;
|
|
23
|
+
let personaRequestedReason;
|
|
24
|
+
for (const chunk of chunks) {
|
|
25
|
+
const memoriesJson = JSON.stringify(chunk.map((m) => ({
|
|
26
|
+
record_id: m.id,
|
|
27
|
+
content: m.content,
|
|
28
|
+
type: m.type,
|
|
29
|
+
priority: m.priority,
|
|
30
|
+
scene_name: m.scene_name,
|
|
31
|
+
// repo 归属(ADR-0015 T1.9/T1.10):L2 场景归属决策可见项目边界——
|
|
32
|
+
// this-repo 记录不得并入跨项目场景;'' = 未归属(不引导分组)。
|
|
33
|
+
repoKeyName: m.repoKeyName ?? '',
|
|
34
|
+
applicability: m.applicability ?? '',
|
|
35
|
+
timestamps: m.timestamps.map((t) => new Date(t).toISOString()),
|
|
36
|
+
})), null, 2);
|
|
37
|
+
const { systemPrompt, userPrompt } = buildScenePrompt({
|
|
38
|
+
memoriesJson,
|
|
39
|
+
sceneSummaries,
|
|
40
|
+
sceneContents,
|
|
41
|
+
currentTimestamp: new Date().toISOString(),
|
|
42
|
+
existingSceneFiles: summaries.map((s) => s.path),
|
|
43
|
+
maxScenes: cfg.l2.maxScenes,
|
|
44
|
+
family,
|
|
45
|
+
});
|
|
52
46
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
const raw = await callLLM(ctx, cfg, {
|
|
48
|
+
system: systemPrompt,
|
|
49
|
+
user: userPrompt,
|
|
50
|
+
maxTokens: resolveLayerTokens(cfg, 'l2'),
|
|
51
|
+
layer: 'l2',
|
|
52
|
+
logger,
|
|
53
|
+
});
|
|
54
|
+
// 解析:先取 PERSONA_UPDATE_REQUEST 标记,再解析操作数组
|
|
55
|
+
const reqMatch = REQUEST_RE.exec(raw);
|
|
56
|
+
if (reqMatch?.[1]?.trim())
|
|
57
|
+
personaRequestedReason = reqMatch[1].trim();
|
|
58
|
+
const opsRaw = reqMatch ? raw.replace(REQUEST_RE, '') : raw;
|
|
59
|
+
const ops = parseJsonLogged(opsRaw, 'L2 场景操作', logger);
|
|
60
|
+
if (!Array.isArray(ops))
|
|
61
|
+
throw new Error('L2 输出不是操作数组');
|
|
62
|
+
for (const op of ops) {
|
|
63
|
+
if (!op || typeof op.path !== 'string')
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
if (op.op === 'write' && typeof op.content === 'string') {
|
|
67
|
+
await scenes.write(op.path, op.content);
|
|
68
|
+
changed++;
|
|
69
|
+
logger.debug?.(`[memory] L2 写入场景 ${op.path} (${op.content.length} 字符)`);
|
|
70
|
+
}
|
|
71
|
+
else if (op.op === 'delete') {
|
|
72
|
+
await scenes.write(op.path, '[DELETED]');
|
|
73
|
+
changed++;
|
|
74
|
+
logger.debug?.(`[memory] L2 删除场景 ${op.path}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger.warn(`[memory] L2 操作失败 ${op.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
62
80
|
}
|
|
63
81
|
}
|
|
64
82
|
catch (err) {
|
|
65
|
-
|
|
83
|
+
// R1 块级容错:单块超时/崩只丢这块,不掀全族(旧行为是整抛)
|
|
84
|
+
failedChunks++;
|
|
85
|
+
logger.warn(`[memory] L2 分块整合失败(family=${family},${chunk.length} 条记忆本块丢弃): ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
continue;
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
|
-
logger.info(`[memory] L2 场景整合完成(family=${family}):${newMemories.length} 条新记忆 → ${changed} 个文件操作(场景总数 ${summaries.length}
|
|
89
|
+
logger.info(`[memory] L2 场景整合完成(family=${family}):${newMemories.length} 条新记忆 → ${changed} 个文件操作(场景总数 ${summaries.length}` +
|
|
90
|
+
(chunks.length > 1 ? `,分 ${chunks.length} 块、失败 ${failedChunks} 块` : '') + `)` +
|
|
69
91
|
(personaRequestedReason ? `,请求 L3 更新:${personaRequestedReason}` : ''));
|
|
70
92
|
return { changed, personaRequestedReason };
|
|
71
93
|
}
|
|
@@ -167,6 +167,8 @@ export class RuminateController {
|
|
|
167
167
|
// 经 runTurn 的 `bucket.push(...messages)` 会重复入桶 → 反刍后待处理计数“还原/变多”。)
|
|
168
168
|
this.runner.enqueue(session.sessionId, [], session.mode, {
|
|
169
169
|
force: true,
|
|
170
|
+
// R6:反刍与 rebuild 同为用户显式动作 → 越过抽取退避,保证"催一把"能真跑不被跳过
|
|
171
|
+
bypassBackoff: true,
|
|
170
172
|
onTurnDone: (records) => {
|
|
171
173
|
this.totalL1 += records;
|
|
172
174
|
this.status.recordsBuilt = this.totalL1;
|
|
@@ -22,6 +22,7 @@ import type { SceneStore } from '../store/scenes.js';
|
|
|
22
22
|
import type { StateStore } from '../store/state.js';
|
|
23
23
|
import type { ConversationMessage, ExtractMode, MemoryFamily, MemoryLogger } from '../types.js';
|
|
24
24
|
import type { FamilyStates } from './l1.js';
|
|
25
|
+
import type { RouteHealthStore } from '../store/route-health.js';
|
|
25
26
|
export interface MemoryStores {
|
|
26
27
|
/**
|
|
27
28
|
* DRSC 客户端(T8.4 读切):蒸馏背景窗的历史供给面(op=session&tail&redact=1)。
|
|
@@ -44,6 +45,8 @@ export interface MemoryStores {
|
|
|
44
45
|
feedback?: {
|
|
45
46
|
rekey(oldId: string, newId: string): boolean;
|
|
46
47
|
};
|
|
48
|
+
/** R9 路由健康学习(可选):未传则 runExtraction 不启用自适应块长学习。 */
|
|
49
|
+
routeHealth?: RouteHealthStore;
|
|
47
50
|
}
|
|
48
51
|
/** 管线任务(优先级调度:live > graph > rebuild)。 */
|
|
49
52
|
export interface PipelineTask {
|
|
@@ -97,6 +100,8 @@ export declare class MemoryRunner {
|
|
|
97
100
|
private extractFailures;
|
|
98
101
|
/** 图谱泵在队列中的占位标志(同一时刻至多一个 graph 任务;护栏四件套之一)。 */
|
|
99
102
|
private graphPumpQueued;
|
|
103
|
+
/** R7:已入队、尚未消费的 force-flush 键集(`session:mode`),同键不重复入队(治积压刷屏)。 */
|
|
104
|
+
private flushQueued;
|
|
100
105
|
private readonly pendingFile;
|
|
101
106
|
/** 分族 checkpoint(init 后可用;重建收尾也从这里读活引用)。 */
|
|
102
107
|
states: FamilyStates;
|
|
@@ -132,6 +137,7 @@ export declare class MemoryRunner {
|
|
|
132
137
|
* onTurnDone 在该任务真正跑完后回调,携带本轮新增记录数(供反刍统计真实产出)。 */
|
|
133
138
|
enqueue(sessionId: string, messages: ConversationMessage[], mode: ExtractMode, opts?: {
|
|
134
139
|
force?: boolean;
|
|
140
|
+
bypassBackoff?: boolean;
|
|
135
141
|
onTurnDone?: (records: number) => void;
|
|
136
142
|
}): void;
|
|
137
143
|
/** 重建任务入队(低优先级:让位于正常轮次;由 RebuildController 分块驱动)。 */
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveDataDir } from '../config.js';
|
|
2
2
|
import { GRAPH_PRIORITY_NEW } from '../graph/types.js';
|
|
3
|
+
import { NoDistillRouteError } from '../llm.js';
|
|
3
4
|
import { startValidityBackfill } from '../validity-backfill.js';
|
|
4
5
|
import { drscRowToConversationMessage } from '../clients/drsc.js';
|
|
5
6
|
import { trace } from '../store/trace.js';
|
|
@@ -83,6 +84,8 @@ export function effectiveCfg(cfg, live) {
|
|
|
83
84
|
: null;
|
|
84
85
|
const layerChains = layerPick && Object.keys(layerPick).length ? layerPick : null;
|
|
85
86
|
const maxInput = s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : null;
|
|
87
|
+
// R10 蒸馏超时运行时覆盖:>0 才注入 llm.timeoutMs,否则跟随静态配置
|
|
88
|
+
const timeoutOv = s && s.distillTimeoutMs > 0 ? s.distillTimeoutMs : null;
|
|
86
89
|
// 旧档位键的全局接管(含给静态回退条目盖章):仅非链模式保留(旧存量值兼容)
|
|
87
90
|
const fallbacksTakeover = !chainMode && eff && cfg.llm?.fallbacks?.length
|
|
88
91
|
? cfg.llm.fallbacks.map((f) => ({ ...f, reasoningEffort: eff }))
|
|
@@ -169,6 +172,7 @@ export function effectiveCfg(cfg, live) {
|
|
|
169
172
|
...(override ?? {}),
|
|
170
173
|
...(budgets ? { budgets } : {}),
|
|
171
174
|
...(maxInput ? { maxInputChars: maxInput } : {}),
|
|
175
|
+
...(timeoutOv ? { timeoutMs: timeoutOv } : {}),
|
|
172
176
|
...(fallbacksTakeover ? { fallbacks: fallbacksTakeover } : {}),
|
|
173
177
|
...(chainEffort ? { primaryEffort: chainEffort } : {}),
|
|
174
178
|
...(chainFallbacks ? { fallbacks: chainFallbacks } : {}),
|
|
@@ -212,6 +216,8 @@ export class MemoryRunner {
|
|
|
212
216
|
extractFailures = new Map();
|
|
213
217
|
/** 图谱泵在队列中的占位标志(同一时刻至多一个 graph 任务;护栏四件套之一)。 */
|
|
214
218
|
graphPumpQueued = false;
|
|
219
|
+
/** R7:已入队、尚未消费的 force-flush 键集(`session:mode`),同键不重复入队(治积压刷屏)。 */
|
|
220
|
+
flushQueued = new Set();
|
|
215
221
|
pendingFile;
|
|
216
222
|
/** 分族 checkpoint(init 后可用;重建收尾也从这里读活引用)。 */
|
|
217
223
|
states;
|
|
@@ -357,9 +363,25 @@ export class MemoryRunner {
|
|
|
357
363
|
/** 一轮对话结束后入队(L0 落盘由 capture 在 turn/end 即时完成,不排蒸馏队列)。
|
|
358
364
|
* onTurnDone 在该任务真正跑完后回调,携带本轮新增记录数(供反刍统计真实产出)。 */
|
|
359
365
|
enqueue(sessionId, messages, mode, opts) {
|
|
366
|
+
// R7:闲置/反刍/换档触发的 force-flush(空消息 + force)按 (session,mode) 去重,
|
|
367
|
+
// 同键未消费前不重复入队(一次长阻塞不再积压几十个 no-op 排空刷屏)。
|
|
368
|
+
const isFlush = opts?.force === true && messages.length === 0;
|
|
369
|
+
const fkey = isFlush ? `${sessionId}:${mode}` : '';
|
|
370
|
+
if (isFlush && this.flushQueued.has(fkey))
|
|
371
|
+
return;
|
|
372
|
+
if (isFlush)
|
|
373
|
+
this.flushQueued.add(fkey);
|
|
360
374
|
this.pushTask({
|
|
361
375
|
kind: 'live',
|
|
362
|
-
run: () =>
|
|
376
|
+
run: async () => {
|
|
377
|
+
try {
|
|
378
|
+
return await this.runTurn(sessionId, messages, mode, opts);
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
if (isFlush)
|
|
382
|
+
this.flushQueued.delete(fkey); // 无论成败都释占,避免键泄漏永久屏蔽后续 flush
|
|
383
|
+
}
|
|
384
|
+
},
|
|
363
385
|
...(opts?.onTurnDone ? { onDone: (r) => opts.onTurnDone?.(typeof r === 'number' ? r : 0) } : {}),
|
|
364
386
|
});
|
|
365
387
|
}
|
|
@@ -637,7 +659,7 @@ export class MemoryRunner {
|
|
|
637
659
|
const effective = effectiveExtractThreshold(this.warmup[mode], cfg.extract.minMessages);
|
|
638
660
|
const sliceLen = bucket.reduce((n, m) => (m.sessionId === sessionId ? n + 1 : n), 0);
|
|
639
661
|
// 抽取失败退避:重建轮(noBufferCap,用户显式动作、有自己的失败/取消 UI)豁免
|
|
640
|
-
const backedOff = !opts?.noBufferCap && this.inExtractBackoff(sessionId);
|
|
662
|
+
const backedOff = !opts?.noBufferCap && !opts?.bypassBackoff && this.inExtractBackoff(sessionId);
|
|
641
663
|
if ((opts?.force || sliceLen >= effective) && !backedOff) {
|
|
642
664
|
newRecords = await this.extractSessionSlice(sessionId, mode, cfg, effective, opts);
|
|
643
665
|
// 新记录入投影队列(优先级恒高于存量补投影)并拉起图谱泵;入队本身廉价,
|
|
@@ -790,9 +812,12 @@ export class MemoryRunner {
|
|
|
790
812
|
(oldId, newId) => {
|
|
791
813
|
this.stores.usage?.rekey(oldId, newId);
|
|
792
814
|
this.stores.feedback?.rekey(oldId, newId);
|
|
793
|
-
});
|
|
815
|
+
}, this.stores.routeHealth);
|
|
816
|
+
// R9: 本轮学到的跑通块长落盘(best-effort,失败不阻蒸馏)
|
|
817
|
+
await this.stores.routeHealth?.save().catch(() => { });
|
|
794
818
|
if (!result.skipped) {
|
|
795
|
-
|
|
819
|
+
// R1′ 部分消费:成功块出桶、抽取失败块留桶下轮重投(rest ∪ failedMessages)
|
|
820
|
+
this.pending[mode] = [...rest, ...(result.failedMessages ?? [])];
|
|
796
821
|
// 重建轮(force)不是有机对话,不推进爬坡
|
|
797
822
|
if (!opts?.force)
|
|
798
823
|
this.warmup[mode] = advanceWarmupThreshold(this.warmup[mode], cfg.extract.minMessages);
|
|
@@ -844,6 +869,12 @@ export class MemoryRunner {
|
|
|
844
869
|
ok: false,
|
|
845
870
|
errorKind: err instanceof Error ? err.name : 'error',
|
|
846
871
|
});
|
|
872
|
+
// R4:无可用蒸馏路由=配置缺失、非模型慢——不进退避(否则把"没配"误当故障反复退避),只顺延。
|
|
873
|
+
if (err instanceof NoDistillRouteError) {
|
|
874
|
+
this.logger.warn(`[memory] 无可用蒸馏路由,本轮蒸馏顺延、不记退避(session=${sessionId},mode=${mode})`);
|
|
875
|
+
await this.persistPending(opts?.noBufferCap).catch(() => { });
|
|
876
|
+
return [];
|
|
877
|
+
}
|
|
847
878
|
// 指数退避:压制闲置兜底/补跑在 LLM 故障期间的连环重试(成功消费时清零)
|
|
848
879
|
const streak = (this.extractFailures.get(sessionId)?.streak ?? 0) + 1;
|
|
849
880
|
const delayMs = extractionBackoffMs(streak);
|
package/dist/settings.js
CHANGED
|
@@ -70,6 +70,7 @@ const ALWAYS_ON = {
|
|
|
70
70
|
distillChain: [],
|
|
71
71
|
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0, graph: 0 },
|
|
72
72
|
distillMaxInputChars: 0,
|
|
73
|
+
distillTimeoutMs: 0,
|
|
73
74
|
distillLayerChains: { l1: [], l2: [], l3: [] },
|
|
74
75
|
distillMode: '',
|
|
75
76
|
directBaseURL: '',
|
|
@@ -309,6 +310,8 @@ function resolveSettings(value) {
|
|
|
309
310
|
graph: num(rawBudgets.graph),
|
|
310
311
|
},
|
|
311
312
|
distillMaxInputChars: num(v.distillMaxInputChars),
|
|
313
|
+
// R10 蒸馏超时运行时覆盖:钳 0~600000(与 live schema 同上限);0 = 跟随部署 llm.timeoutMs
|
|
314
|
+
distillTimeoutMs: Math.min(600_000, Math.max(0, num(v.distillTimeoutMs))),
|
|
312
315
|
// 蒸馏通道:mode 白名单(非法归 '' = 跟随部署);端点与密钥截断防御但保留原始内容
|
|
313
316
|
distillMode: v.distillMode === 'host' || v.distillMode === 'direct' ? v.distillMode : '',
|
|
314
317
|
directBaseURL: typeof v.directBaseURL === 'string' ? v.directBaseURL.slice(0, 2000) : '',
|
package/dist/stats.js
CHANGED
|
@@ -1344,6 +1344,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1344
1344
|
toolRoom: true, toolGraph: true, toolRuminate: true, toolConflict: true, toolMutate: true, toolSkills: true,
|
|
1345
1345
|
reasoningEffort: '', distillProvider: '', distillModel: '', distillChain: [],
|
|
1346
1346
|
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0, graph: 0 }, distillMaxInputChars: 0,
|
|
1347
|
+
distillTimeoutMs: 0,
|
|
1347
1348
|
distillLayerChains: { l1: [], l2: [], l3: [] },
|
|
1348
1349
|
distillMode: '', directBaseURL: '', directApiKey: '',
|
|
1349
1350
|
embedRemoteBaseURL: '', embedRemoteApiKey: '', embedRemoteModel: '', embedRemoteDimensions: 0,
|
|
@@ -1386,6 +1387,12 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1386
1387
|
fallback: cfg.llm.maxInputChars,
|
|
1387
1388
|
effective: s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : cfg.llm.maxInputChars,
|
|
1388
1389
|
},
|
|
1390
|
+
// R10 蒸馏超时(ms):current 是运行时覆盖(0 = 跟随配置),fallback 是静态 llm.timeoutMs
|
|
1391
|
+
timeoutBudget: {
|
|
1392
|
+
current: s?.distillTimeoutMs ?? 0,
|
|
1393
|
+
fallback: cfg.llm.timeoutMs,
|
|
1394
|
+
effective: s && s.distillTimeoutMs > 0 ? s.distillTimeoutMs : cfg.llm.timeoutMs,
|
|
1395
|
+
},
|
|
1389
1396
|
};
|
|
1390
1397
|
return resp;
|
|
1391
1398
|
}
|
|
@@ -1468,14 +1475,22 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1468
1475
|
}
|
|
1469
1476
|
clean.distillBudgets = budgets;
|
|
1470
1477
|
}
|
|
1471
|
-
// 输入预算(字符):0 = 跟随静态配置;正值须落在静态 schema 同款范围(1000~
|
|
1478
|
+
// 输入预算(字符):0 = 跟随静态配置;正值须落在静态 schema 同款范围(1000~256k)
|
|
1472
1479
|
if (patch.distillMaxInputChars !== undefined) {
|
|
1473
1480
|
const n = Number(patch.distillMaxInputChars);
|
|
1474
|
-
if (!Number.isInteger(n) || n < 0 || n >
|
|
1475
|
-
throw new Error('distillMaxInputChars 须为 0 或 1000~
|
|
1481
|
+
if (!Number.isInteger(n) || n < 0 || n > 256_000 || (n > 0 && n < 1000)) {
|
|
1482
|
+
throw new Error('distillMaxInputChars 须为 0 或 1000~256000 的整数(0 = 跟随配置)');
|
|
1476
1483
|
}
|
|
1477
1484
|
clean.distillMaxInputChars = n;
|
|
1478
1485
|
}
|
|
1486
|
+
// R10 蒸馏超时(ms):0 = 跟随静态配置;正值 1000~600000(与 live schema 同上限)
|
|
1487
|
+
if (patch.distillTimeoutMs !== undefined) {
|
|
1488
|
+
const n = Number(patch.distillTimeoutMs);
|
|
1489
|
+
if (!Number.isInteger(n) || n < 0 || n > 600_000 || (n > 0 && n < 1000)) {
|
|
1490
|
+
throw new Error('distillTimeoutMs 须为 0 或 1000~600000 的整数(0 = 跟随配置)');
|
|
1491
|
+
}
|
|
1492
|
+
clean.distillTimeoutMs = n;
|
|
1493
|
+
}
|
|
1479
1494
|
// 蒸馏通道运行时覆盖(direct 解耦):mode 白名单;endpoint 可回显;apiKey 属机密——
|
|
1480
1495
|
// 写入后存 settings,但不回读到 UI、不落本条日志(下方日志先剔除后记录)
|
|
1481
1496
|
if (patch.distillMode !== undefined) {
|
|
@@ -29,3 +29,5 @@ export declare const PENDING_FILE_VERSION = 1;
|
|
|
29
29
|
export declare const RECONCILE_STATE_FILE_VERSION = 1;
|
|
30
30
|
/** `mem-md-ledger.json`(md 投射指纹账本,N9;派生态,删了可重建)。*/
|
|
31
31
|
export declare const MEM_MD_LEDGER_FILE_VERSION = 1;
|
|
32
|
+
/** `distill-route-health.json`(蒸馏路由健康学习值,R9;运行时派生,删了可重学)。*/
|
|
33
|
+
export declare const ROUTE_HEALTH_FILE_VERSION = 1;
|
|
@@ -29,3 +29,5 @@ export const PENDING_FILE_VERSION = 1;
|
|
|
29
29
|
export const RECONCILE_STATE_FILE_VERSION = 1;
|
|
30
30
|
/** `mem-md-ledger.json`(md 投射指纹账本,N9;派生态,删了可重建)。*/
|
|
31
31
|
export const MEM_MD_LEDGER_FILE_VERSION = 1;
|
|
32
|
+
/** `distill-route-health.json`(蒸馏路由健康学习值,R9;运行时派生,删了可重学)。*/
|
|
33
|
+
export const ROUTE_HEALTH_FILE_VERSION = 1;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** 单条路由健康记录。 */
|
|
2
|
+
export interface RouteHealthEntry {
|
|
3
|
+
/** 退避后实测跑通的最小块长(字符);0 = 未学到。 */
|
|
4
|
+
lastGoodChunkChars: number;
|
|
5
|
+
/** 该路由延迟 EMA(ms);0 = 未记。R5 自适应可据此调初值。 */
|
|
6
|
+
latencyEmaMs: number;
|
|
7
|
+
/** 最近一次记录时刻(ISO)。 */
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
}
|
|
10
|
+
/** 学习值默认过期(ms):超此龄视为陈旧、读侧应重探(换模型/网关不至于被历史最小值锁死)。 */
|
|
11
|
+
export declare const ROUTE_HEALTH_STALE_MS: number;
|
|
12
|
+
export declare function routeHealthPathFor(dataDir: string): string;
|
|
13
|
+
export declare class RouteHealthStore {
|
|
14
|
+
private readonly file;
|
|
15
|
+
private routes;
|
|
16
|
+
private dirty;
|
|
17
|
+
constructor(file: string);
|
|
18
|
+
/** 读盘 fail-open:缺/坏/未知版本 → 空表、不抛。 */
|
|
19
|
+
load(): Promise<void>;
|
|
20
|
+
/** 该路由已学到、且未过期的跑通块长;0 = 无可用学习值(未学到或已陈旧)。 */
|
|
21
|
+
lastGoodChunkChars(provider: string, model: string, now?: number, staleMs?: number): number;
|
|
22
|
+
/** 该路由延迟 EMA(ms);0 = 未记。 */
|
|
23
|
+
latencyEma(provider: string, model: string): number;
|
|
24
|
+
/**
|
|
25
|
+
* 记一次「跑通」的块长:只降不升(更小块长才覆盖 lastGood),并更新延迟 EMA。
|
|
26
|
+
* @param chunkChars 本次成功的块长(字符)
|
|
27
|
+
* @param latencyMs 本次成功调用耗时(可选,用于 EMA)
|
|
28
|
+
*/
|
|
29
|
+
recordGood(provider: string, model: string, chunkChars: number, latencyMs?: number): void;
|
|
30
|
+
/** 清除学习值:传 provider+model 清单条,否则清全部(面板「重置学习值」一键)。 */
|
|
31
|
+
reset(provider?: string, model?: string): void;
|
|
32
|
+
/** 全量快照(面板回显用)。 */
|
|
33
|
+
snapshot(): Array<{
|
|
34
|
+
key: string;
|
|
35
|
+
} & RouteHealthEntry>;
|
|
36
|
+
/** 有变更才落盘;失败由调用方兜底(不抛穿蒸馏)。@returns 是否写了盘 */
|
|
37
|
+
save(): Promise<boolean>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏路由健康学习(R9):按 `provider/model` 记「自适应退避后实测跑通的最小块长」,
|
|
3
|
+
* 跨进程留存,令下次 perChunk 初值直接取跑通过的值、不必每进程重付退避风暴;
|
|
4
|
+
* 面板可回显「实测最小可用块长 / 最后退避长度」。
|
|
5
|
+
*
|
|
6
|
+
* 运行时学习值、非磁盘契约:
|
|
7
|
+
* - 读 **fail-open**(缺文件/损坏/未知版本 → 空表起步,绝不抛、不阻蒸馏——学习值丢了最多少一次优化);
|
|
8
|
+
* - 写走 `atomicWriteJson`(原子 + 覆盖保护);
|
|
9
|
+
* - **只降不升**:更小的跑通块长才覆盖 lastGood(换 provider/过期由读侧决定重探,见 R5 接线)。
|
|
10
|
+
*
|
|
11
|
+
* 本模块 Phase 1 仅落地存储与纯逻辑,尚未接线到 runExtraction(接线在 Phase 4 R5/R9)。
|
|
12
|
+
*/
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { atomicWriteJson, readJsonStrict } from '../util/io.js';
|
|
15
|
+
import { ROUTE_HEALTH_FILE_VERSION } from './file-versions.js';
|
|
16
|
+
function keyOf(provider, model) {
|
|
17
|
+
return `${provider}/${model}`;
|
|
18
|
+
}
|
|
19
|
+
/** 学习值默认过期(ms):超此龄视为陈旧、读侧应重探(换模型/网关不至于被历史最小值锁死)。 */
|
|
20
|
+
export const ROUTE_HEALTH_STALE_MS = 7 * 24 * 60 * 60_000;
|
|
21
|
+
export function routeHealthPathFor(dataDir) {
|
|
22
|
+
return path.join(dataDir, 'distill-route-health.json');
|
|
23
|
+
}
|
|
24
|
+
export class RouteHealthStore {
|
|
25
|
+
file;
|
|
26
|
+
routes = new Map();
|
|
27
|
+
dirty = false;
|
|
28
|
+
constructor(file) {
|
|
29
|
+
this.file = file;
|
|
30
|
+
}
|
|
31
|
+
/** 读盘 fail-open:缺/坏/未知版本 → 空表、不抛。 */
|
|
32
|
+
async load() {
|
|
33
|
+
const r = await readJsonStrict(this.file, { expectedVersion: ROUTE_HEALTH_FILE_VERSION });
|
|
34
|
+
this.routes = new Map();
|
|
35
|
+
if (!r.ok)
|
|
36
|
+
return; // missing / corrupt / unreadable / unknown_version 一律空起步
|
|
37
|
+
const raw = r.value?.routes;
|
|
38
|
+
if (!raw || typeof raw !== 'object')
|
|
39
|
+
return;
|
|
40
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
41
|
+
if (!v || typeof v !== 'object')
|
|
42
|
+
continue;
|
|
43
|
+
const e = v;
|
|
44
|
+
if (typeof e.lastGoodChunkChars !== 'number' || !Number.isFinite(e.lastGoodChunkChars))
|
|
45
|
+
continue;
|
|
46
|
+
this.routes.set(k, {
|
|
47
|
+
lastGoodChunkChars: Math.max(0, Math.floor(e.lastGoodChunkChars)),
|
|
48
|
+
latencyEmaMs: typeof e.latencyEmaMs === 'number' && Number.isFinite(e.latencyEmaMs) ? e.latencyEmaMs : 0,
|
|
49
|
+
updatedAt: typeof e.updatedAt === 'string' ? e.updatedAt : '',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** 该路由已学到、且未过期的跑通块长;0 = 无可用学习值(未学到或已陈旧)。 */
|
|
54
|
+
lastGoodChunkChars(provider, model, now = Date.now(), staleMs = ROUTE_HEALTH_STALE_MS) {
|
|
55
|
+
const e = this.routes.get(keyOf(provider, model));
|
|
56
|
+
if (!e || e.lastGoodChunkChars <= 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const t = Date.parse(e.updatedAt);
|
|
59
|
+
if (Number.isFinite(t) && now - t > staleMs)
|
|
60
|
+
return 0; // 陈旧 → 交读侧重探
|
|
61
|
+
return e.lastGoodChunkChars;
|
|
62
|
+
}
|
|
63
|
+
/** 该路由延迟 EMA(ms);0 = 未记。 */
|
|
64
|
+
latencyEma(provider, model) {
|
|
65
|
+
return this.routes.get(keyOf(provider, model))?.latencyEmaMs ?? 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 记一次「跑通」的块长:只降不升(更小块长才覆盖 lastGood),并更新延迟 EMA。
|
|
69
|
+
* @param chunkChars 本次成功的块长(字符)
|
|
70
|
+
* @param latencyMs 本次成功调用耗时(可选,用于 EMA)
|
|
71
|
+
*/
|
|
72
|
+
recordGood(provider, model, chunkChars, latencyMs) {
|
|
73
|
+
if (!(chunkChars > 0) || !Number.isFinite(chunkChars))
|
|
74
|
+
return;
|
|
75
|
+
const k = keyOf(provider, model);
|
|
76
|
+
const cur = this.routes.get(k);
|
|
77
|
+
const next = {
|
|
78
|
+
lastGoodChunkChars: cur && cur.lastGoodChunkChars > 0
|
|
79
|
+
? Math.min(cur.lastGoodChunkChars, Math.floor(chunkChars))
|
|
80
|
+
: Math.floor(chunkChars),
|
|
81
|
+
latencyEmaMs: latencyMs !== undefined && Number.isFinite(latencyMs) && latencyMs >= 0
|
|
82
|
+
? cur && cur.latencyEmaMs > 0
|
|
83
|
+
? cur.latencyEmaMs * 0.7 + latencyMs * 0.3
|
|
84
|
+
: latencyMs
|
|
85
|
+
: (cur?.latencyEmaMs ?? 0),
|
|
86
|
+
updatedAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
this.routes.set(k, next);
|
|
89
|
+
this.dirty = true;
|
|
90
|
+
}
|
|
91
|
+
/** 清除学习值:传 provider+model 清单条,否则清全部(面板「重置学习值」一键)。 */
|
|
92
|
+
reset(provider, model) {
|
|
93
|
+
if (provider && model)
|
|
94
|
+
this.routes.delete(keyOf(provider, model));
|
|
95
|
+
else
|
|
96
|
+
this.routes.clear();
|
|
97
|
+
this.dirty = true;
|
|
98
|
+
}
|
|
99
|
+
/** 全量快照(面板回显用)。 */
|
|
100
|
+
snapshot() {
|
|
101
|
+
return [...this.routes.entries()].map(([key, v]) => ({ key, ...v }));
|
|
102
|
+
}
|
|
103
|
+
/** 有变更才落盘;失败由调用方兜底(不抛穿蒸馏)。@returns 是否写了盘 */
|
|
104
|
+
async save() {
|
|
105
|
+
if (!this.dirty)
|
|
106
|
+
return false;
|
|
107
|
+
const routes = {};
|
|
108
|
+
for (const [k, v] of this.routes)
|
|
109
|
+
routes[k] = v;
|
|
110
|
+
const payload = { version: ROUTE_HEALTH_FILE_VERSION, routes };
|
|
111
|
+
await atomicWriteJson(this.file, payload);
|
|
112
|
+
this.dirty = false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
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.9",
|
|
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.
|
|
3
|
+
"version": "0.26.9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org",
|
|
6
6
|
"access": "public",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
66
|
"node": ">=22.16.0",
|
|
67
|
-
"dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2
|
|
67
|
+
"dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2"
|
|
68
68
|
},
|
|
69
69
|
"keywords": [
|
|
70
70
|
"dsh",
|