dsh-prime-memory 0.26.5 → 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/CHANGELOG.md +39 -0
- package/README.en.md +62 -1
- package/README.md +60 -2
- package/dist/client.js +45 -5
- package/dist/config.d.ts +40 -0
- package/dist/config.js +33 -3
- package/dist/contract.d.ts +14 -1
- package/dist/embedding-child.cjs +192 -0
- package/dist/embedding-worker.cjs +2 -0
- package/dist/graph/apply.d.ts +9 -0
- package/dist/graph/apply.js +12 -2
- package/dist/graph/search.d.ts +11 -2
- package/dist/graph/search.js +38 -10
- package/dist/hooks/recall.js +28 -1
- package/dist/index.js +14 -1
- package/dist/llm.d.ts +21 -2
- package/dist/llm.js +56 -25
- package/dist/pipeline/graph.d.ts +17 -1
- package/dist/pipeline/graph.js +45 -2
- package/dist/pipeline/l1.d.ts +15 -2
- package/dist/pipeline/l1.js +161 -47
- package/dist/pipeline/l2.js +72 -50
- package/dist/pipeline/md-sync.js +4 -2
- package/dist/pipeline/ruminate.js +2 -0
- package/dist/pipeline/runner.d.ts +32 -0
- package/dist/pipeline/runner.js +109 -11
- package/dist/pipeline/slot-migrate.d.ts +3 -2
- package/dist/pipeline/slot-migrate.js +3 -2
- package/dist/projection/recall.d.ts +10 -3
- package/dist/projection/recall.js +14 -0
- package/dist/settings.js +6 -0
- package/dist/stats.d.ts +1 -1
- package/dist/stats.js +35 -14
- package/dist/store/embedding-source.d.ts +4 -2
- package/dist/store/embedding-source.js +5 -2
- package/dist/store/file-versions.d.ts +2 -0
- package/dist/store/file-versions.js +2 -0
- package/dist/store/graph-store.d.ts +71 -1
- package/dist/store/graph-store.js +221 -8
- package/dist/store/l1.d.ts +5 -0
- package/dist/store/l1.js +18 -2
- package/dist/store/local-embedding.d.ts +9 -5
- package/dist/store/local-embedding.js +137 -17
- package/dist/store/memory-md-disable.d.ts +3 -2
- package/dist/store/memory-md.d.ts +3 -3
- package/dist/store/memory-md.js +6 -4
- package/dist/store/route-health.d.ts +38 -0
- package/dist/store/route-health.js +115 -0
- package/dist/store/runtime-installer.d.ts +13 -1
- package/dist/store/runtime-installer.js +40 -2
- package/dist/store/sqlite.d.ts +24 -12
- package/dist/store/sqlite.js +97 -31
- package/dist/store/supersede.d.ts +10 -2
- package/dist/store/supersede.js +10 -2
- package/dist/store/validity.d.ts +150 -0
- package/dist/store/validity.js +209 -0
- package/dist/tools/index.d.ts +15 -1
- package/dist/tools/index.js +393 -11
- package/dist/util/json-repair.d.ts +48 -0
- package/dist/util/json-repair.js +285 -0
- package/dist/validity-backfill.d.ts +78 -0
- package/dist/validity-backfill.js +160 -0
- package/dsh.plugin.json +2 -2
- package/package.json +3 -3
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
|
}
|
package/dist/pipeline/md-sync.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { opendir, readFile, realpath, stat } from 'node:fs/promises';
|
|
34
34
|
import { join, resolve, sep } from 'node:path';
|
|
35
|
-
import { DISABLE_SUFFIX, digest, recordDigest, targetFor } from '../store/memory-md.js';
|
|
35
|
+
import { DISABLE_SUFFIX, digest, isRetiredRecord, recordDigest, targetFor } from '../store/memory-md.js';
|
|
36
36
|
import { exportRecords } from '../store/memory-md-export.js';
|
|
37
37
|
import { parseMdFile } from '../store/memory-md-parse.js';
|
|
38
38
|
import { normalizeContentMd } from '../util/md-normalize.js';
|
|
@@ -360,7 +360,9 @@ export async function syncFromFiles(deps) {
|
|
|
360
360
|
// 自校验(N13):`.disable` 也带 `hash`。**只留痕、不改裁决**——后缀即“人已判过时”,
|
|
361
361
|
// 若因凭证正文被手改一字节就把退场锁死,等于让人改坏凭证就丢掉处置意图。
|
|
362
362
|
noteHash(parseMdFile(text), abs);
|
|
363
|
-
|
|
363
|
+
// 门1 解耦:退场判据是 marker 单源(`isRetiredRecord` 与库侧同口径)——
|
|
364
|
+
// retire 不再闭 valid_to,拿 validTo 判"已退场"会让第二轮重复计数、永不收敛到 same。
|
|
365
|
+
if (isRetiredRecord(rec)) {
|
|
364
366
|
report.same += 1;
|
|
365
367
|
if (entry === undefined)
|
|
366
368
|
await stampDisabled(ledger, rec, abs, text);
|
|
@@ -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 分块驱动)。 */
|
|
@@ -158,6 +164,32 @@ export declare class MemoryRunner {
|
|
|
158
164
|
* 每轮 drain 至多消费一个 graph 任务且永远让位 live)。
|
|
159
165
|
*/
|
|
160
166
|
startGraphBackfillTimer(): void;
|
|
167
|
+
/**
|
|
168
|
+
* validity 标注存量回填的接线(task_6.3):低频、让位于蒸馏——启动 3 分钟后首轮,
|
|
169
|
+
* 之后每 6h 一次(首轮收敛后多数轮是纯扫描 no-op;defer 的部分自动续上)。
|
|
170
|
+
* 门控跟灰度闸走:tieringEnabled(部署 ∨ 面板)关时不进任务——功能关着时
|
|
171
|
+
* 存量 metadata 逐字不变(零漂移,与 keyword-backfill 的门控同法)。
|
|
172
|
+
*/
|
|
173
|
+
startValidityBackfillTimer(): void;
|
|
174
|
+
private runValidityBackfillTick;
|
|
175
|
+
/**
|
|
176
|
+
* 确定性写面的公共入队钩子(graph-write-plane R2):非蒸馏写入路径
|
|
177
|
+
* (`memory_add`/`memory_import`/`memory_update`)落库成功后把涉及记录投进
|
|
178
|
+
* 投影队——与 distill 分支同源同优先级,闭合"只有蒸馏才投影"的旁路缺口
|
|
179
|
+
* (用户手加记录长期 projected=None 的真根因)。部署未开图谱时 no-op,
|
|
180
|
+
* 用户后续开启由启动/周期补投影兜底(与 distill 分支同一口径)。
|
|
181
|
+
*/
|
|
182
|
+
enqueueGraphFor(recordIds: readonly string[]): void;
|
|
183
|
+
/**
|
|
184
|
+
* 强制重投的写面入口(graph-write-plane R3):把上游 R10 的内部
|
|
185
|
+
* `forceReprojectRecord` 接到泵上——删凭证、高优入队、拉泵;幂等由
|
|
186
|
+
* 在途 mapping 的 PK 去重保证。图谱未启用时直接回零(不开"投了永不泵"
|
|
187
|
+
* 的陈旧队列口子,纠正通道的语义前提是图谱通路存在)。
|
|
188
|
+
*/
|
|
189
|
+
reprojectGraph(recordIds: readonly string[]): {
|
|
190
|
+
queued: number;
|
|
191
|
+
jobIds: string[];
|
|
192
|
+
};
|
|
161
193
|
/**
|
|
162
194
|
* 图谱投影泵入队判定:
|
|
163
195
|
* - 双门:部署级 cfg.graph.enabled + 运行时 live.enabled/distill,任一为假不入队;
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { resolveDataDir } from '../config.js';
|
|
2
2
|
import { GRAPH_PRIORITY_NEW } from '../graph/types.js';
|
|
3
|
+
import { NoDistillRouteError } from '../llm.js';
|
|
4
|
+
import { startValidityBackfill } from '../validity-backfill.js';
|
|
3
5
|
import { drscRowToConversationMessage } from '../clients/drsc.js';
|
|
4
6
|
import { trace } from '../store/trace.js';
|
|
5
7
|
import { emptyPending, freshWarmup, groupPendingBySession, loadPending, PENDING_MODES, pendingPathFor, savePending, } from '../store/pending.js';
|
|
@@ -82,6 +84,8 @@ export function effectiveCfg(cfg, live) {
|
|
|
82
84
|
: null;
|
|
83
85
|
const layerChains = layerPick && Object.keys(layerPick).length ? layerPick : null;
|
|
84
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;
|
|
85
89
|
// 旧档位键的全局接管(含给静态回退条目盖章):仅非链模式保留(旧存量值兼容)
|
|
86
90
|
const fallbacksTakeover = !chainMode && eff && cfg.llm?.fallbacks?.length
|
|
87
91
|
? cfg.llm.fallbacks.map((f) => ({ ...f, reasoningEffort: eff }))
|
|
@@ -136,6 +140,11 @@ export function effectiveCfg(cfg, live) {
|
|
|
136
140
|
const mpOverride = s?.memportEnabled !== undefined
|
|
137
141
|
? { memport: { ...(cfg.memport ?? {}), enabled: cfg.memport?.enabled === true || s.memportEnabled === true } }
|
|
138
142
|
: null;
|
|
143
|
+
// 时效分级门2 灰度闸:live.validityTiering 与部署 cfg.validity.tieringEnabled 取或
|
|
144
|
+
// (同一先例)。两字都假时回 `{tieringEnabled:false}` 与缺省形状等价,注入侧零漂移不破。
|
|
145
|
+
const vtOverride = s?.validityTiering !== undefined
|
|
146
|
+
? { validity: { ...(cfg.validity ?? {}), tieringEnabled: cfg.validity?.tieringEnabled === true || s.validityTiering === true } }
|
|
147
|
+
: null;
|
|
139
148
|
// md 投射/后缀收敛的单次动作护栏运行时覆盖(0 = 跟随部署 cfg.memExport.maxRecords)。
|
|
140
149
|
// 只覆盖条数护栏,不动 enabled/root——开关与根路径仍属部署配置。
|
|
141
150
|
const mdx = s?.mdExportMaxRecords ?? 0;
|
|
@@ -144,7 +153,7 @@ export function effectiveCfg(cfg, live) {
|
|
|
144
153
|
// 不动 maxRecords/timeBudgetMs/callTimeoutMs——那三个仍属部署护栏,要改得改 YAML。
|
|
145
154
|
const bf = s?.backfillConcurrency ?? 0;
|
|
146
155
|
const backfillOverride = bf > 0 ? { backfill: { ...(cfg.backfill ?? {}), concurrency: bf } } : null;
|
|
147
|
-
if (!override && !budgets && !maxInput && !fallbacksTakeover && !chainEffort && !chainFallbacks && !effortInject && !layerChains && !hasChannel && !embedOverride && !cfOverride && !memExportOverride && !backfillOverride && !kwOverride && !rdOverride && !mdsOverride && !mpOverride)
|
|
156
|
+
if (!override && !budgets && !maxInput && !fallbacksTakeover && !chainEffort && !chainFallbacks && !effortInject && !layerChains && !hasChannel && !embedOverride && !cfOverride && !memExportOverride && !backfillOverride && !kwOverride && !rdOverride && !mdsOverride && !mpOverride && !vtOverride)
|
|
148
157
|
return cfg;
|
|
149
158
|
return {
|
|
150
159
|
...cfg,
|
|
@@ -154,6 +163,7 @@ export function effectiveCfg(cfg, live) {
|
|
|
154
163
|
...(rdOverride ?? {}),
|
|
155
164
|
...(mdsOverride ?? {}),
|
|
156
165
|
...(mpOverride ?? {}),
|
|
166
|
+
...(vtOverride ?? {}),
|
|
157
167
|
...(memExportOverride ?? {}),
|
|
158
168
|
...(backfillOverride ?? {}),
|
|
159
169
|
llm: {
|
|
@@ -162,6 +172,7 @@ export function effectiveCfg(cfg, live) {
|
|
|
162
172
|
...(override ?? {}),
|
|
163
173
|
...(budgets ? { budgets } : {}),
|
|
164
174
|
...(maxInput ? { maxInputChars: maxInput } : {}),
|
|
175
|
+
...(timeoutOv ? { timeoutMs: timeoutOv } : {}),
|
|
165
176
|
...(fallbacksTakeover ? { fallbacks: fallbacksTakeover } : {}),
|
|
166
177
|
...(chainEffort ? { primaryEffort: chainEffort } : {}),
|
|
167
178
|
...(chainFallbacks ? { fallbacks: chainFallbacks } : {}),
|
|
@@ -205,6 +216,8 @@ export class MemoryRunner {
|
|
|
205
216
|
extractFailures = new Map();
|
|
206
217
|
/** 图谱泵在队列中的占位标志(同一时刻至多一个 graph 任务;护栏四件套之一)。 */
|
|
207
218
|
graphPumpQueued = false;
|
|
219
|
+
/** R7:已入队、尚未消费的 force-flush 键集(`session:mode`),同键不重复入队(治积压刷屏)。 */
|
|
220
|
+
flushQueued = new Set();
|
|
208
221
|
pendingFile;
|
|
209
222
|
/** 分族 checkpoint(init 后可用;重建收尾也从这里读活引用)。 */
|
|
210
223
|
states;
|
|
@@ -350,9 +363,25 @@ export class MemoryRunner {
|
|
|
350
363
|
/** 一轮对话结束后入队(L0 落盘由 capture 在 turn/end 即时完成,不排蒸馏队列)。
|
|
351
364
|
* onTurnDone 在该任务真正跑完后回调,携带本轮新增记录数(供反刍统计真实产出)。 */
|
|
352
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);
|
|
353
374
|
this.pushTask({
|
|
354
375
|
kind: 'live',
|
|
355
|
-
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
|
+
},
|
|
356
385
|
...(opts?.onTurnDone ? { onDone: (r) => opts.onTurnDone?.(typeof r === 'number' ? r : 0) } : {}),
|
|
357
386
|
});
|
|
358
387
|
}
|
|
@@ -454,6 +483,65 @@ export class MemoryRunner {
|
|
|
454
483
|
return () => clearInterval(timer);
|
|
455
484
|
});
|
|
456
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* validity 标注存量回填的接线(task_6.3):低频、让位于蒸馏——启动 3 分钟后首轮,
|
|
488
|
+
* 之后每 6h 一次(首轮收敛后多数轮是纯扫描 no-op;defer 的部分自动续上)。
|
|
489
|
+
* 门控跟灰度闸走:tieringEnabled(部署 ∨ 面板)关时不进任务——功能关着时
|
|
490
|
+
* 存量 metadata 逐字不变(零漂移,与 keyword-backfill 的门控同法)。
|
|
491
|
+
*/
|
|
492
|
+
startValidityBackfillTimer() {
|
|
493
|
+
this.ctx.effect(() => {
|
|
494
|
+
let loop = null;
|
|
495
|
+
const first = setTimeout(() => {
|
|
496
|
+
this.runValidityBackfillTick();
|
|
497
|
+
loop = setInterval(() => this.runValidityBackfillTick(), 6 * 3_600_000);
|
|
498
|
+
}, 180_000);
|
|
499
|
+
return () => { clearTimeout(first); if (loop !== null)
|
|
500
|
+
clearInterval(loop); };
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
runValidityBackfillTick() {
|
|
504
|
+
if (this.stopped)
|
|
505
|
+
return;
|
|
506
|
+
const eff = effectiveCfg(this.cfg, this.live);
|
|
507
|
+
if (eff.validity?.tieringEnabled !== true)
|
|
508
|
+
return;
|
|
509
|
+
startValidityBackfill({
|
|
510
|
+
l1: this.stores.l1,
|
|
511
|
+
logger: this.logger,
|
|
512
|
+
// task_7.7 接缝:回填改过标注的记录顺手重投保鲜图谱(图谱未启用时 reprojectGraph 自己回零)。
|
|
513
|
+
reproject: (ids) => { this.reprojectGraph(ids); },
|
|
514
|
+
shouldCancel: () => this.stopped,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 确定性写面的公共入队钩子(graph-write-plane R2):非蒸馏写入路径
|
|
519
|
+
* (`memory_add`/`memory_import`/`memory_update`)落库成功后把涉及记录投进
|
|
520
|
+
* 投影队——与 distill 分支同源同优先级,闭合"只有蒸馏才投影"的旁路缺口
|
|
521
|
+
* (用户手加记录长期 projected=None 的真根因)。部署未开图谱时 no-op,
|
|
522
|
+
* 用户后续开启由启动/周期补投影兜底(与 distill 分支同一口径)。
|
|
523
|
+
*/
|
|
524
|
+
enqueueGraphFor(recordIds) {
|
|
525
|
+
if (recordIds.length === 0 || !this.stores.graph || !this.cfg.graph?.enabled)
|
|
526
|
+
return;
|
|
527
|
+
this.stores.graph.queueGraphProjection([...recordIds], GRAPH_PRIORITY_NEW);
|
|
528
|
+
this.maybeQueueGraphTask();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 强制重投的写面入口(graph-write-plane R3):把上游 R10 的内部
|
|
532
|
+
* `forceReprojectRecord` 接到泵上——删凭证、高优入队、拉泵;幂等由
|
|
533
|
+
* 在途 mapping 的 PK 去重保证。图谱未启用时直接回零(不开"投了永不泵"
|
|
534
|
+
* 的陈旧队列口子,纠正通道的语义前提是图谱通路存在)。
|
|
535
|
+
*/
|
|
536
|
+
reprojectGraph(recordIds) {
|
|
537
|
+
const graphs = this.stores.graph;
|
|
538
|
+
if (!graphs || !this.cfg.graph?.enabled || recordIds.length === 0)
|
|
539
|
+
return { queued: 0, jobIds: [] };
|
|
540
|
+
const r = graphs.forceReprojectRecord(recordIds);
|
|
541
|
+
if (r.queued > 0)
|
|
542
|
+
this.maybeQueueGraphTask();
|
|
543
|
+
return r;
|
|
544
|
+
}
|
|
457
545
|
/**
|
|
458
546
|
* 图谱投影泵入队判定:
|
|
459
547
|
* - 双门:部署级 cfg.graph.enabled + 运行时 live.enabled/distill,任一为假不入队;
|
|
@@ -478,7 +566,10 @@ export class MemoryRunner {
|
|
|
478
566
|
this.graphPumpQueued = false;
|
|
479
567
|
if (this.stopped)
|
|
480
568
|
return;
|
|
481
|
-
const had = await runGraphProjection(this.ctx, effectiveCfg(this.cfg, this.live), graphs, this.logger
|
|
569
|
+
const had = await runGraphProjection(this.ctx, effectiveCfg(this.cfg, this.live), graphs, this.logger,
|
|
570
|
+
// G-vec 缺陷修的嵌入缝:惰性取 L1 同一嵌入源(活切换不被钉死),
|
|
571
|
+
// 未就绪/失败 embedText 回 undefined = 不补写(词法路不受影响)。
|
|
572
|
+
(t) => this.stores.l1.embedText(t));
|
|
482
573
|
// 有实际投影(说明队列还有积压)才续排;claim 落空则停泵,等下一个触发源
|
|
483
574
|
if (had)
|
|
484
575
|
this.maybeQueueGraphTask();
|
|
@@ -568,15 +659,13 @@ export class MemoryRunner {
|
|
|
568
659
|
const effective = effectiveExtractThreshold(this.warmup[mode], cfg.extract.minMessages);
|
|
569
660
|
const sliceLen = bucket.reduce((n, m) => (m.sessionId === sessionId ? n + 1 : n), 0);
|
|
570
661
|
// 抽取失败退避:重建轮(noBufferCap,用户显式动作、有自己的失败/取消 UI)豁免
|
|
571
|
-
const backedOff = !opts?.noBufferCap && this.inExtractBackoff(sessionId);
|
|
662
|
+
const backedOff = !opts?.noBufferCap && !opts?.bypassBackoff && this.inExtractBackoff(sessionId);
|
|
572
663
|
if ((opts?.force || sliceLen >= effective) && !backedOff) {
|
|
573
664
|
newRecords = await this.extractSessionSlice(sessionId, mode, cfg, effective, opts);
|
|
574
665
|
// 新记录入投影队列(优先级恒高于存量补投影)并拉起图谱泵;入队本身廉价,
|
|
575
|
-
// 部署未开图谱(cfg.graph.enabled)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
this.maybeQueueGraphTask();
|
|
579
|
-
}
|
|
666
|
+
// 部署未开图谱(cfg.graph.enabled)时不入队——用户后续开启由启动补投影兜底。
|
|
667
|
+
// (写面 R2:分支本体抽成 enqueueGraphFor,与工具三路径同源,不再两处漂移。)
|
|
668
|
+
this.enqueueGraphFor(newRecords.map((r) => r.id));
|
|
580
669
|
}
|
|
581
670
|
else {
|
|
582
671
|
this.logger.debug?.(backedOff
|
|
@@ -723,9 +812,12 @@ export class MemoryRunner {
|
|
|
723
812
|
(oldId, newId) => {
|
|
724
813
|
this.stores.usage?.rekey(oldId, newId);
|
|
725
814
|
this.stores.feedback?.rekey(oldId, newId);
|
|
726
|
-
});
|
|
815
|
+
}, this.stores.routeHealth);
|
|
816
|
+
// R9: 本轮学到的跑通块长落盘(best-effort,失败不阻蒸馏)
|
|
817
|
+
await this.stores.routeHealth?.save().catch(() => { });
|
|
727
818
|
if (!result.skipped) {
|
|
728
|
-
|
|
819
|
+
// R1′ 部分消费:成功块出桶、抽取失败块留桶下轮重投(rest ∪ failedMessages)
|
|
820
|
+
this.pending[mode] = [...rest, ...(result.failedMessages ?? [])];
|
|
729
821
|
// 重建轮(force)不是有机对话,不推进爬坡
|
|
730
822
|
if (!opts?.force)
|
|
731
823
|
this.warmup[mode] = advanceWarmupThreshold(this.warmup[mode], cfg.extract.minMessages);
|
|
@@ -777,6 +869,12 @@ export class MemoryRunner {
|
|
|
777
869
|
ok: false,
|
|
778
870
|
errorKind: err instanceof Error ? err.name : 'error',
|
|
779
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
|
+
}
|
|
780
878
|
// 指数退避:压制闲置兜底/补跑在 LLM 故障期间的连环重试(成功消费时清零)
|
|
781
879
|
const streak = (this.extractFailures.get(sessionId)?.streak ?? 0) + 1;
|
|
782
880
|
const delayMs = extractionBackoffMs(streak);
|
|
@@ -22,11 +22,12 @@ export interface SlotMigrateStoreFace {
|
|
|
22
22
|
list(): Slot[];
|
|
23
23
|
close(id: string, status: 'done' | 'dropped'): Promise<boolean>;
|
|
24
24
|
}
|
|
25
|
-
/** 迁移需要的 L1
|
|
25
|
+
/** 迁移需要的 L1 只读面(门1 解耦:退场判据是 marker 单源——metadata 带 `dsh_superseded`
|
|
26
|
+
* 即已退场;valid_to 只是现实有效期,带着它不算退场)。 */
|
|
26
27
|
export interface SlotMigrateL1Face {
|
|
27
28
|
getByIds(ids: string[]): Array<{
|
|
28
29
|
id: string;
|
|
29
|
-
|
|
30
|
+
metadata?: unknown;
|
|
30
31
|
}>;
|
|
31
32
|
}
|
|
32
33
|
/** 一条可迁移槽位(预览用,执行前即可展示)。 */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errDetail } from '../util/filelog.js';
|
|
2
|
+
import { readSupersedeMarker } from '../store/supersede.js';
|
|
2
3
|
/** 判定 + 挑选可迁移槽位(纯读,无副作用;面板预览与反刍执行共用)。 */
|
|
3
4
|
export function pickMigratableSlots(slots, l1) {
|
|
4
5
|
if (l1 === undefined)
|
|
@@ -6,9 +7,9 @@ export function pickMigratableSlots(slots, l1) {
|
|
|
6
7
|
const open = slots.filter((s) => s.status === 'open' && s.pinned === true && s.kind === 'rule');
|
|
7
8
|
if (open.length === 0)
|
|
8
9
|
return [];
|
|
9
|
-
// 一次批量取全部 refs,避免 N
|
|
10
|
+
// 一次批量取全部 refs,避免 N 次查询。存活 = 无退场 marker(门1 解耦同口径)。
|
|
10
11
|
const allRefs = [...new Set(open.flatMap((s) => s.refs ?? []))];
|
|
11
|
-
const alive = new Set(l1.getByIds(allRefs).filter((r) => r.
|
|
12
|
+
const alive = new Set(l1.getByIds(allRefs).filter((r) => readSupersedeMarker(r.metadata) === undefined).map((r) => r.id));
|
|
12
13
|
const out = [];
|
|
13
14
|
for (const s of open) {
|
|
14
15
|
const carrier = (s.refs ?? []).find((r) => alive.has(r));
|
|
@@ -58,6 +58,13 @@ export interface RecallItemView {
|
|
|
58
58
|
* 读侧据此把「已被摘要取代的旧召回」与「此刻仍在上下文里的召回」区分开。
|
|
59
59
|
*/
|
|
60
60
|
superseded?: true;
|
|
61
|
+
/**
|
|
62
|
+
* 时效定性快照(task_3.5,expiry-tiering 门2 展示面):仅在 `expired`/`expired_suspect`
|
|
63
|
+
* 两档写键(泡泡如实标"已于 X 失效/疑似失效");current/historical 等缺席=旧帧兼容。
|
|
64
|
+
*/
|
|
65
|
+
validity?: import('../store/validity.js').ValidityState;
|
|
66
|
+
/** 现实有效期(ISO):与 `validity` 同进同退,只在写 validity 时带上。 */
|
|
67
|
+
validUntil?: string;
|
|
61
68
|
}
|
|
62
69
|
/** 持久态:本会话的活召回 id 集 + 判脏序号。 */
|
|
63
70
|
export interface RecallProjectionState {
|
|
@@ -89,13 +96,13 @@ interface SchemaLike<T> {
|
|
|
89
96
|
}
|
|
90
97
|
/** state 校验(registry 用它挡掉损坏/过期的 checkpoint 行)。 */
|
|
91
98
|
export declare const stateSchema: SchemaLike<RecallProjectionState>;
|
|
92
|
-
/** view 校验(出网前调用;未知键容忍,同 slots 投影的语义边界)。 */
|
|
93
99
|
export declare const viewSchema: SchemaLike<RecallView>;
|
|
94
100
|
/** 投影需要的 L1 只读面(结构化声明,避免跨模块类型耦合)。
|
|
95
|
-
* 判退场要 `validTo`/`metadata
|
|
101
|
+
* 判退场要 `validTo`/`metadata`,时效定性还要 `persistence`/`validFrom`,
|
|
102
|
+
* 故直接 Pick 自 MemoryRecord,不在投影里自写时间比较。
|
|
96
103
|
* `name` 是名称蒸馏产出的短标题,优先作为展示标题。 */
|
|
97
104
|
export interface RecallL1Face {
|
|
98
|
-
getByIds(ids: string[]): Array<Pick<MemoryRecord, 'id' | 'type' | 'content' | 'priority' | 'metadata' | 'validTo' | 'name'>>;
|
|
105
|
+
getByIds(ids: string[]): Array<Pick<MemoryRecord, 'id' | 'type' | 'content' | 'priority' | 'metadata' | 'validFrom' | 'validTo' | 'persistence' | 'name'>>;
|
|
99
106
|
}
|
|
100
107
|
/** 投影需要的反馈只读面(缺席 = view 不带 rating,旧行为)。 */
|
|
101
108
|
export interface RecallFeedbackFace {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRetired } from '../store/supersede.js';
|
|
2
|
+
import { stateOfRecord } from '../store/validity.js';
|
|
2
3
|
export const MEMORY_RECALL_KEY = 'memoryRecall';
|
|
3
4
|
/** view 最多透出条数(最近在上;超出部分只计入 count,防 wire 体量失控)。 */
|
|
4
5
|
export const RECALL_VIEW_CAP = 20;
|
|
@@ -43,6 +44,7 @@ export const stateSchema = {
|
|
|
43
44
|
},
|
|
44
45
|
};
|
|
45
46
|
/** view 校验(出网前调用;未知键容忍,同 slots 投影的语义边界)。 */
|
|
47
|
+
const VALIDITY_STATES_VIEW = ['current', 'historical', 'expired', 'expired_suspect', 'superseded', 'retired'];
|
|
46
48
|
export const viewSchema = {
|
|
47
49
|
parse: (value) => {
|
|
48
50
|
const what = 'memoryRecall.view';
|
|
@@ -73,6 +75,12 @@ export const viewSchema = {
|
|
|
73
75
|
invalid(at, 'retired 只能是 true 或缺席');
|
|
74
76
|
if (item.superseded !== undefined && item.superseded !== true)
|
|
75
77
|
invalid(at, 'superseded 只能是 true 或缺席');
|
|
78
|
+
// 时效标注(task_3.5):缺席=旧帧容忍;有则必须六态枚举内,validUntil 必字符串。
|
|
79
|
+
if (item.validity !== undefined && VALIDITY_STATES_VIEW.indexOf(item.validity) < 0) {
|
|
80
|
+
invalid(at, 'validity 必须是六态之一或缺席');
|
|
81
|
+
}
|
|
82
|
+
if (item.validUntil !== undefined)
|
|
83
|
+
requireString(item.validUntil, at, 'validUntil');
|
|
76
84
|
});
|
|
77
85
|
return value;
|
|
78
86
|
},
|
|
@@ -150,6 +158,11 @@ function makeView(state, l1, feedback) {
|
|
|
150
158
|
? r.metadata?.dsh_usage_scenario
|
|
151
159
|
: undefined;
|
|
152
160
|
const rating = feedback?.lastOf(id);
|
|
161
|
+
// 时效标注(task_3.5):只对"过时两档"写键——current/historical 不必让人看见时效噪声;
|
|
162
|
+
// 旧帧无此键,读侧缺席即活跃。判定现算(stateOfRecord),不吃落库快照;
|
|
163
|
+
// 未退场行才会真判过时两档(退场行由 retired 键表达,validity 不再叠加)。
|
|
164
|
+
const vState = isRetired(r) ? undefined : stateOfRecord(r, Date.now());
|
|
165
|
+
const showValidity = vState === 'expired' || vState === 'expired_suspect';
|
|
153
166
|
items.push({
|
|
154
167
|
id: r.id,
|
|
155
168
|
type: r.type,
|
|
@@ -162,6 +175,7 @@ function makeView(state, l1, feedback) {
|
|
|
162
175
|
...(isRetired(r) ? { retired: true } : {}),
|
|
163
176
|
// 已被摘要取代:正文已不在上下文里。与 retired 正交,两者可同时在。
|
|
164
177
|
...(superseded.has(id) ? { superseded: true } : {}),
|
|
178
|
+
...(showValidity ? { validity: vState, validUntil: r.validTo === undefined ? '' : new Date(r.validTo).toISOString() } : {}),
|
|
165
179
|
});
|
|
166
180
|
}
|
|
167
181
|
const next = { rev: state.rev, sessionId: state.sessionId, count: state.ids.length, items };
|
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: '',
|
|
@@ -85,6 +86,8 @@ const ALWAYS_ON = {
|
|
|
85
86
|
redistillEnabled: false,
|
|
86
87
|
mdSyncEnabled: false,
|
|
87
88
|
memportEnabled: false,
|
|
89
|
+
// 时效分级门2 灰度开关:默认关(注入与改前逐字一致)
|
|
90
|
+
validityTiering: false,
|
|
88
91
|
// md 投射护栏运行时覆盖:0 = 跟随部署 cfg.memExport.maxRecords(面板上可改)
|
|
89
92
|
mdExportMaxRecords: 0,
|
|
90
93
|
// 回填并行度运行时覆盖:0 = 跟随部署 cfg.backfill.concurrency(概览页滑块可改)
|
|
@@ -307,6 +310,8 @@ function resolveSettings(value) {
|
|
|
307
310
|
graph: num(rawBudgets.graph),
|
|
308
311
|
},
|
|
309
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))),
|
|
310
315
|
// 蒸馏通道:mode 白名单(非法归 '' = 跟随部署);端点与密钥截断防御但保留原始内容
|
|
311
316
|
distillMode: v.distillMode === 'host' || v.distillMode === 'direct' ? v.distillMode : '',
|
|
312
317
|
directBaseURL: typeof v.directBaseURL === 'string' ? v.directBaseURL.slice(0, 2000) : '',
|
|
@@ -329,6 +334,7 @@ function resolveSettings(value) {
|
|
|
329
334
|
redistillEnabled: v.redistillEnabled === true,
|
|
330
335
|
mdSyncEnabled: v.mdSyncEnabled === true,
|
|
331
336
|
memportEnabled: v.memportEnabled === true,
|
|
337
|
+
validityTiering: v.validityTiering === true,
|
|
332
338
|
// md 投射护栏钳 0~10000(与 live schema 同上限);0 = 跟随部署默认
|
|
333
339
|
mdExportMaxRecords: Math.min(10_000, Math.max(0, num(v.mdExportMaxRecords))),
|
|
334
340
|
// 回填并行度钳 0~8(与 live schema 同上限);0 = 跟随部署 cfg.backfill.concurrency
|
package/dist/stats.d.ts
CHANGED
|
@@ -183,7 +183,7 @@ export declare function hitToUiRecord(r: {
|
|
|
183
183
|
scope?: string;
|
|
184
184
|
/** 归属工作区路径(仅 scope='workspace' 时非空)。 */
|
|
185
185
|
workspaceId?: string;
|
|
186
|
-
/**
|
|
186
|
+
/** 退场判据:与库侧同源(门1 解耦后 marker 单源,`isRetired`)。由 `listL1`/`getByIds` 透传。 */
|
|
187
187
|
validTo?: number;
|
|
188
188
|
}, extra?: {
|
|
189
189
|
agingWeight?: number | null;
|