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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 容错 JSON 解析:LLM 输出常带小语法毛刺,一次 JSON.parse 定生死会把整批记忆全丢。
|
|
3
|
+
*
|
|
4
|
+
* 三级链(见 #10):
|
|
5
|
+
* strict → 原样 JSON.parse,零回归;
|
|
6
|
+
* repaired → 字符串态扫描,做四类**只动语法不动内容**的确定性修复:
|
|
7
|
+
* 1) 内容里的裸 ASCII `"` → `\"`(判据:该 `"` 后第一个非空白字符不是 `,` `}` `]` `:` `"` EOF)
|
|
8
|
+
* 2) 字符串里的裸 `\n` `\r` `\t` → 转义
|
|
9
|
+
* 3) `,` 紧跟 `}` `]` 的尾逗号 → 删
|
|
10
|
+
* 4) 结尾未闭合的字符串与括号 → 补齐(对应 finish=length 截断)
|
|
11
|
+
* salvaged → 整体修不动时按顶层元素边界逐个 parse,保住坏点前后已完整的对象,
|
|
12
|
+
* 尾巴丢掉(不猜)。
|
|
13
|
+
*
|
|
14
|
+
* 修复/救援发生时会带上 `notes`,由调用方决定是否 warn 落盘(便于回溯:修复是兜底不是常态)。
|
|
15
|
+
*/
|
|
16
|
+
/** 判断 idx 处的 `"` 是否是「真结束」:后一个非空白字符是结构字符或 EOF。 */
|
|
17
|
+
function isRealStringClose(src, idx) {
|
|
18
|
+
for (let j = idx + 1; j < src.length; j++) {
|
|
19
|
+
const c = src[j];
|
|
20
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r')
|
|
21
|
+
continue;
|
|
22
|
+
return c === ',' || c === '}' || c === ']' || c === ':';
|
|
23
|
+
}
|
|
24
|
+
return true; // EOF
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 字符串态扫描修复:只动语法,字段名与值内容字节一个不改。
|
|
28
|
+
* 返回 null 表示扫描中遇到无法处理的结构错误(交给上层救援)。
|
|
29
|
+
*/
|
|
30
|
+
export function repairJson(src) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const notes = [];
|
|
33
|
+
const stack = [];
|
|
34
|
+
let inStr = false;
|
|
35
|
+
let escaped = false;
|
|
36
|
+
let bareQuoteFixed = 0;
|
|
37
|
+
let ctrlCharEscaped = 0;
|
|
38
|
+
let trailingCommaDropped = 0;
|
|
39
|
+
for (let i = 0; i < src.length; i++) {
|
|
40
|
+
const c = src[i];
|
|
41
|
+
if (inStr) {
|
|
42
|
+
if (escaped) {
|
|
43
|
+
// 上一个字符是 `\`,当前是转义内容的一部分,原样输出并复位
|
|
44
|
+
out.push(c);
|
|
45
|
+
escaped = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (c === '\\') {
|
|
49
|
+
out.push(c);
|
|
50
|
+
escaped = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (c === '"') {
|
|
54
|
+
if (isRealStringClose(src, i)) {
|
|
55
|
+
out.push(c);
|
|
56
|
+
inStr = false;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
out.push('\\"');
|
|
60
|
+
bareQuoteFixed++;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === '\n') {
|
|
65
|
+
out.push('\\n');
|
|
66
|
+
ctrlCharEscaped++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '\r') {
|
|
70
|
+
out.push('\\r');
|
|
71
|
+
ctrlCharEscaped++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === '\t') {
|
|
75
|
+
out.push('\\t');
|
|
76
|
+
ctrlCharEscaped++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
out.push(c);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// ── 结构态 ──
|
|
83
|
+
if (c === '"') {
|
|
84
|
+
inStr = true;
|
|
85
|
+
out.push(c);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (c === '{' || c === '[') {
|
|
89
|
+
stack.push(c);
|
|
90
|
+
out.push(c);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (c === '}' || c === ']') {
|
|
94
|
+
// 弹出匹配的 opener;若不匹配也尽力弹一层(常见于模型写错一层)
|
|
95
|
+
if (stack.length > 0)
|
|
96
|
+
stack.pop();
|
|
97
|
+
// 处理尾逗号:回删 out 末尾的 `,` 与其前面的空白
|
|
98
|
+
const before = trimTrailingComma(out);
|
|
99
|
+
if (before > 0)
|
|
100
|
+
trailingCommaDropped += before;
|
|
101
|
+
out.push(c);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === ',') {
|
|
105
|
+
// 前瞻:下一个非空白字符若是 `}` `]` → 尾逗号,吞掉
|
|
106
|
+
let j = i + 1;
|
|
107
|
+
while (j < src.length && (src[j] === ' ' || src[j] === '\t' || src[j] === '\n' || src[j] === '\r'))
|
|
108
|
+
j++;
|
|
109
|
+
if (j < src.length && (src[j] === '}' || src[j] === ']')) {
|
|
110
|
+
trailingCommaDropped++;
|
|
111
|
+
continue; // 不输出这个逗号
|
|
112
|
+
}
|
|
113
|
+
out.push(c);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
out.push(c);
|
|
117
|
+
}
|
|
118
|
+
let tail = out.join('');
|
|
119
|
+
// 结尾未闭合的字符串 → 补 `"`
|
|
120
|
+
if (inStr) {
|
|
121
|
+
tail += '"';
|
|
122
|
+
notes.push('补闭合未终止的字符串');
|
|
123
|
+
}
|
|
124
|
+
// 结尾未闭合的括号 → 按栈反向补齐
|
|
125
|
+
while (stack.length > 0) {
|
|
126
|
+
const opener = stack.pop();
|
|
127
|
+
tail += opener === '{' ? '}' : ']';
|
|
128
|
+
}
|
|
129
|
+
if (bareQuoteFixed > 0)
|
|
130
|
+
notes.push(`转义裸引号 ${bareQuoteFixed} 处`);
|
|
131
|
+
if (ctrlCharEscaped > 0)
|
|
132
|
+
notes.push(`转义控制字符 ${ctrlCharEscaped} 处`);
|
|
133
|
+
if (trailingCommaDropped > 0)
|
|
134
|
+
notes.push(`删尾逗号 ${trailingCommaDropped} 处`);
|
|
135
|
+
return { out: tail, notes };
|
|
136
|
+
}
|
|
137
|
+
/** out buffer 尾部若为 `,...` 结构(已推入的字符),移除尾部逗号与空白,返回删除的逗号数。 */
|
|
138
|
+
function trimTrailingComma(out) {
|
|
139
|
+
let dropped = 0;
|
|
140
|
+
while (out.length > 0) {
|
|
141
|
+
const top = out[out.length - 1];
|
|
142
|
+
if (top === ' ' || top === '\t' || top === '\n' || top === '\r') {
|
|
143
|
+
out.pop();
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (top === ',') {
|
|
147
|
+
out.pop();
|
|
148
|
+
dropped++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
// 处理 `,,}` 双逗号:递归检查
|
|
154
|
+
if (dropped > 0)
|
|
155
|
+
dropped += trimTrailingComma(out);
|
|
156
|
+
return dropped;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 顶层逐元素救援:仅当输入是 `[...]` 且整体 parse 失败时启用。
|
|
160
|
+
* 按结构态(非字符串)找顶层逗号,切片成一个个元素,分别 JSON.parse,失败的丢掉。
|
|
161
|
+
*/
|
|
162
|
+
export function salvageTopLevelElements(src) {
|
|
163
|
+
const first = src.indexOf('[');
|
|
164
|
+
if (first < 0)
|
|
165
|
+
return null;
|
|
166
|
+
const items = [];
|
|
167
|
+
let dropped = 0;
|
|
168
|
+
let depth = 0;
|
|
169
|
+
let inStr = false;
|
|
170
|
+
let escaped = false;
|
|
171
|
+
let segStart = first + 1;
|
|
172
|
+
for (let i = first; i < src.length; i++) {
|
|
173
|
+
const c = src[i];
|
|
174
|
+
if (inStr) {
|
|
175
|
+
if (escaped) {
|
|
176
|
+
escaped = false;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (c === '\\') {
|
|
180
|
+
escaped = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (c === '"')
|
|
184
|
+
inStr = false;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (c === '"') {
|
|
188
|
+
inStr = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (c === '[' || c === '{') {
|
|
192
|
+
depth++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (c === ']' || c === '}') {
|
|
196
|
+
depth--;
|
|
197
|
+
if (depth === 0 && i === src.length - 1) { /* 数组收尾 */ }
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (c === ',' && depth === 1) {
|
|
201
|
+
const seg = src.slice(segStart, i).trim();
|
|
202
|
+
if (seg) {
|
|
203
|
+
try {
|
|
204
|
+
items.push(JSON.parse(seg));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
dropped++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
segStart = i + 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 最后一段(到 `]` 前)
|
|
214
|
+
const closing = src.lastIndexOf(']');
|
|
215
|
+
const lastSeg = src.slice(segStart, closing > segStart ? closing : src.length).trim();
|
|
216
|
+
if (lastSeg) {
|
|
217
|
+
try {
|
|
218
|
+
items.push(JSON.parse(lastSeg));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
dropped++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (items.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
return { items, dropped };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 预处理:剥 ```json```围栏 + 切到首个 `[`/`{` 直到末尾 `]`/`}`,兼容 LLM 前后附的散文。
|
|
230
|
+
* 与旧版 parseJson 逐字行为一致(零回归)。
|
|
231
|
+
*/
|
|
232
|
+
export function preprocessLLMJson(raw) {
|
|
233
|
+
let s = raw.trim();
|
|
234
|
+
if (s.startsWith('```')) {
|
|
235
|
+
s = s.replace(/^```[a-zA-Z]*\n?/, '').replace(/```$/, '').trim();
|
|
236
|
+
}
|
|
237
|
+
const start = s.indexOf('[');
|
|
238
|
+
const brace = s.indexOf('{');
|
|
239
|
+
let begin;
|
|
240
|
+
if (start === -1)
|
|
241
|
+
begin = brace;
|
|
242
|
+
else if (brace === -1)
|
|
243
|
+
begin = start;
|
|
244
|
+
else
|
|
245
|
+
begin = Math.min(start, brace);
|
|
246
|
+
if (begin > 0)
|
|
247
|
+
s = s.slice(begin);
|
|
248
|
+
const end = s.lastIndexOf(']') > s.lastIndexOf('}') ? s.lastIndexOf(']') + 1 : s.lastIndexOf('}') + 1;
|
|
249
|
+
if (end > 0)
|
|
250
|
+
s = s.slice(0, end);
|
|
251
|
+
return s;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 三级容错入口。传入已通过「剥围栏 + 切片」的字符串,返回解析结果与档位说明。
|
|
255
|
+
* 若三级皆失败,抛出 strict JSON.parse 的原始错误。
|
|
256
|
+
*/
|
|
257
|
+
export function parseJsonTolerant(raw) {
|
|
258
|
+
const s = preprocessLLMJson(raw);
|
|
259
|
+
// L1 strict
|
|
260
|
+
try {
|
|
261
|
+
return { value: JSON.parse(s), level: 'strict', notes: [] };
|
|
262
|
+
}
|
|
263
|
+
catch (strictErr) {
|
|
264
|
+
// L2 repaired
|
|
265
|
+
const rep = repairJson(s);
|
|
266
|
+
if (rep) {
|
|
267
|
+
try {
|
|
268
|
+
return { value: JSON.parse(rep.out), level: 'repaired', notes: rep.notes };
|
|
269
|
+
}
|
|
270
|
+
catch { /* fall through */ }
|
|
271
|
+
}
|
|
272
|
+
// L3 salvaged(仅当形如数组时启用)
|
|
273
|
+
if (s.trim().startsWith('[')) {
|
|
274
|
+
// 先试修复后再救援
|
|
275
|
+
const salv = salvageTopLevelElements(rep?.out ?? s);
|
|
276
|
+
if (salv) {
|
|
277
|
+
const notes = [`逐元素救援成功,保住 ${salv.items.length} 条、丢弃 ${salv.dropped} 条`];
|
|
278
|
+
if (rep?.notes.length)
|
|
279
|
+
notes.unshift(...rep.notes);
|
|
280
|
+
return { value: salv.items, level: 'salvaged', notes };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
throw strictErr;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存量 `dsh_validity` 标注回填(expiry-tiering task_6.2 / 6.3 / 7.7)。
|
|
3
|
+
*
|
|
4
|
+
* 与 keyword/wing 回填同族但**零 LLM**——classifyValidity 是纯函数,本回填是
|
|
5
|
+
* 本地派生:分批扫存量 L1,对"缺标注或标注与现算不符"的行写回 `dsh_validity`
|
|
6
|
+
* 快照,每攒一批对被改记录调 `forceReprojectRecord` 让图谱保鲜(task_7.7 的接缝)。
|
|
7
|
+
*
|
|
8
|
+
* 安全边界(继承回填族四件套):
|
|
9
|
+
* - 单飞:同时最多一个任务(重复触发返回 running,不排队不叠加);
|
|
10
|
+
* - 后台执行 + 可中断:逐页推进、`shouldCancel`/墙钟预算/条数上限三闸,跑了多少算多少,
|
|
11
|
+
* 其余 deferred 留待下次触发(低频周期或面板);
|
|
12
|
+
* - 失败可退:单行 patch 失败只计数,原记录零改动;
|
|
13
|
+
* - 门控:`validity.tieringEnabled`(部署 ∨ 面板)关时不跑——标注面与消费面同开同关,
|
|
14
|
+
* 功能关着时存量 metadata 逐字不变(零漂移)。
|
|
15
|
+
*/
|
|
16
|
+
import type { L1Store } from './store/l1.js';
|
|
17
|
+
import { stateOfRecord } from './store/validity.js';
|
|
18
|
+
import type { MemoryLogger } from './types.js';
|
|
19
|
+
export interface ValidityBackfillOutcome {
|
|
20
|
+
started: boolean;
|
|
21
|
+
running: boolean;
|
|
22
|
+
scanned: number;
|
|
23
|
+
updated: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
/** 预算/上限用尽留下的条数(下次周期续跑)。 */
|
|
26
|
+
deferred: number;
|
|
27
|
+
cancelled: boolean;
|
|
28
|
+
/** 累计因本次回填而被重投入队的记录数(图谱保鲜面)。 */
|
|
29
|
+
reprojected: number;
|
|
30
|
+
}
|
|
31
|
+
interface RunState extends ValidityBackfillOutcome {
|
|
32
|
+
scanned: number;
|
|
33
|
+
}
|
|
34
|
+
/** 当前进度快照(面板/日志读;无任务在跑返回 null)。 */
|
|
35
|
+
export declare function validityBackfillState(): RunState | null;
|
|
36
|
+
/** 请求中止本轮(在跑的任务在下一个检查点收工;空闲时零效果)。 */
|
|
37
|
+
export declare function cancelValidityBackfill(): {
|
|
38
|
+
cancelled: boolean;
|
|
39
|
+
running: boolean;
|
|
40
|
+
};
|
|
41
|
+
export interface ValidityBackfillDeps {
|
|
42
|
+
l1: L1Store;
|
|
43
|
+
logger: MemoryLogger;
|
|
44
|
+
/**
|
|
45
|
+
* 强制重投缝(runner.reprojectGraph:图谱未启用时自己回零,本模块不碰图谱门)。
|
|
46
|
+
* 只对被改过标注的记录调用——没动的记录不需要重投(图谱读时派生不吃 dsh_validity)。
|
|
47
|
+
*/
|
|
48
|
+
reproject: (recordIds: readonly string[]) => void;
|
|
49
|
+
maxRecords?: number;
|
|
50
|
+
timeBudgetMs?: number;
|
|
51
|
+
/** 外部中止判据(宿主 dispose / 面板取消);每行前检查一次。 */
|
|
52
|
+
shouldCancel?: () => boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 启动后台回填;已在运行返回 {started:false}(单飞)。
|
|
56
|
+
* 完成状态留在 `validityBackfillState()` 供最后一次读取(下轮启动时重置)。
|
|
57
|
+
*/
|
|
58
|
+
export declare function startValidityBackfill(deps: ValidityBackfillDeps): {
|
|
59
|
+
started: boolean;
|
|
60
|
+
running: boolean;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* 回填主体(导出供单测 await 到完成;生产走 `startValidityBackfill` 的单飞包装)。
|
|
64
|
+
* 同步扫描 + 同步写回(L1Store 的读面都是同步的),只在页间 `yieldLoop` 让位——
|
|
65
|
+
* 没有 LLM 的回填不需要并发池,串行就是最快且最不会打脏的形状。
|
|
66
|
+
*/
|
|
67
|
+
export declare function runValidityBackfillOnce(deps: ValidityBackfillDeps): Promise<ValidityBackfillOutcome>;
|
|
68
|
+
/** 单行是否需要补标/重标(导出供单测钉判据;生产路径走 runValidityBackfillOnce)。 */
|
|
69
|
+
export declare function validityBackfillNeeds(metadata: unknown, record: Parameters<typeof stateOfRecord>[0], now: number): boolean;
|
|
70
|
+
/** 内部常量导出(测试钉预算/批形)。 */
|
|
71
|
+
export declare const VALIDITY_BACKFILL_TUNING: {
|
|
72
|
+
readonly PAGE: 200;
|
|
73
|
+
readonly DEFAULT_MAX_RECORDS: 5000;
|
|
74
|
+
readonly DEFAULT_TIME_BUDGET_MS: 120000;
|
|
75
|
+
readonly REPROJECT_BATCH: 50;
|
|
76
|
+
readonly VALIDITY_METADATA_KEY: "dsh_validity";
|
|
77
|
+
};
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readValidity, VALIDITY_METADATA_KEY, stampValidity, stateOfRecord } from './store/validity.js';
|
|
2
|
+
import { yieldLoop } from './util/yield.js';
|
|
3
|
+
/** 扫描页大小(轻量投影分页;与 keyword-backfill 一致)。 */
|
|
4
|
+
const PAGE = 200;
|
|
5
|
+
/** 单次任务默认处理上限(真机库 ~千级,一轮可清完大半;剩余 defer)。 */
|
|
6
|
+
const DEFAULT_MAX_RECORDS = 5_000;
|
|
7
|
+
/** 默认墙钟预算(ms):纯本地派生 + 索引点查,比 LLM 回填便宜几个量级。 */
|
|
8
|
+
const DEFAULT_TIME_BUDGET_MS = 120_000;
|
|
9
|
+
/** 每攒多少条被改记录触发一次图谱重投(task_7.7 的"每批")。 */
|
|
10
|
+
const REPROJECT_BATCH = 50;
|
|
11
|
+
let current = null;
|
|
12
|
+
let abortRequested = false;
|
|
13
|
+
/** 当前进度快照(面板/日志读;无任务在跑返回 null)。 */
|
|
14
|
+
export function validityBackfillState() {
|
|
15
|
+
return current === null ? null : { ...current };
|
|
16
|
+
}
|
|
17
|
+
/** 请求中止本轮(在跑的任务在下一个检查点收工;空闲时零效果)。 */
|
|
18
|
+
export function cancelValidityBackfill() {
|
|
19
|
+
if (current === null || !current.running)
|
|
20
|
+
return { cancelled: false, running: false };
|
|
21
|
+
abortRequested = true;
|
|
22
|
+
return { cancelled: true, running: true };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 启动后台回填;已在运行返回 {started:false}(单飞)。
|
|
26
|
+
* 完成状态留在 `validityBackfillState()` 供最后一次读取(下轮启动时重置)。
|
|
27
|
+
*/
|
|
28
|
+
export function startValidityBackfill(deps) {
|
|
29
|
+
if (current?.running)
|
|
30
|
+
return { started: false, running: true };
|
|
31
|
+
current = {
|
|
32
|
+
started: true, running: true, scanned: 0, updated: 0, failed: 0,
|
|
33
|
+
deferred: 0, cancelled: false, reprojected: 0,
|
|
34
|
+
};
|
|
35
|
+
abortRequested = false;
|
|
36
|
+
void runValidityBackfillOnce(deps).finally(() => {
|
|
37
|
+
if (current !== null) {
|
|
38
|
+
current.running = false;
|
|
39
|
+
deps.logger.info(`[memory] validity 回填结束:扫描 ${current.scanned} 行,补标 ${current.updated} 条` +
|
|
40
|
+
(current.failed > 0 ? `,失败 ${current.failed} 条` : '') +
|
|
41
|
+
(current.reprojected > 0 ? `,重投 ${current.reprojected} 条` : '') +
|
|
42
|
+
(current.deferred > 0 ? `,留待下次 ${current.deferred} 条` : ''));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return { started: true, running: true };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 回填主体(导出供单测 await 到完成;生产走 `startValidityBackfill` 的单飞包装)。
|
|
49
|
+
* 同步扫描 + 同步写回(L1Store 的读面都是同步的),只在页间 `yieldLoop` 让位——
|
|
50
|
+
* 没有 LLM 的回填不需要并发池,串行就是最快且最不会打脏的形状。
|
|
51
|
+
*/
|
|
52
|
+
export async function runValidityBackfillOnce(deps) {
|
|
53
|
+
const { l1, logger } = deps;
|
|
54
|
+
abortRequested = false;
|
|
55
|
+
const maxRecords = Math.max(1, deps.maxRecords ?? DEFAULT_MAX_RECORDS);
|
|
56
|
+
const timeBudgetMs = Math.max(1_000, deps.timeBudgetMs ?? DEFAULT_TIME_BUDGET_MS);
|
|
57
|
+
const startedAt = Date.now();
|
|
58
|
+
// 新一轮必须重置计数:上一轮的 current 只置 running=false 不清空(留面板终读)——
|
|
59
|
+
// 直接调 runOnce(不经 start 的重置)若复用旧对象,scanned/updated 会跨轮累加。
|
|
60
|
+
const st = (current === null || current.running === false)
|
|
61
|
+
? {
|
|
62
|
+
started: true, running: true, scanned: 0, updated: 0, failed: 0, deferred: 0, cancelled: false, reprojected: 0,
|
|
63
|
+
}
|
|
64
|
+
: current;
|
|
65
|
+
current = st;
|
|
66
|
+
const flushReproject = (ids) => {
|
|
67
|
+
if (ids.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
// task_7.7:每批标注写完即保鲜——forceReprojectRecord 自身的 PK 去重保证
|
|
70
|
+
// "在途不堆重",回填重复跑同一批也只会多投一次凭证删除,不会堆 job。
|
|
71
|
+
try {
|
|
72
|
+
deps.reproject(ids);
|
|
73
|
+
st.reprojected += ids.length;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
logger.warn(`[memory] validity 回填补投失败(忽略,图谱由周期 sweep 兜底): ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const dirty = [];
|
|
80
|
+
let offset = 0;
|
|
81
|
+
let hitCeil = false;
|
|
82
|
+
for (;;) {
|
|
83
|
+
if (abortRequested || deps.shouldCancel?.() === true) {
|
|
84
|
+
st.cancelled = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (st.scanned >= maxRecords) {
|
|
88
|
+
hitCeil = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (Date.now() - startedAt > timeBudgetMs) {
|
|
92
|
+
hitCeil = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const page = l1.allLite(PAGE, offset);
|
|
96
|
+
if (page.length === 0)
|
|
97
|
+
break;
|
|
98
|
+
offset += page.length;
|
|
99
|
+
// allLite 只有 id/type/metadata——classify 要吃 validTo/persistence,整页 hydrate
|
|
100
|
+
const byId = new Map(l1.getByIds(page.map((p) => p.id)).map((r) => [r.id, r]));
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
for (const lite of page) {
|
|
103
|
+
// 逐行检查点("可中断"承诺的粒度):页间检查在千行页上形同虚设。
|
|
104
|
+
if (abortRequested || deps.shouldCancel?.() === true) {
|
|
105
|
+
st.cancelled = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const rec = byId.get(lite.id);
|
|
109
|
+
if (rec === undefined)
|
|
110
|
+
continue; // 行在扫描瞬间被删:回填不追墓碑
|
|
111
|
+
st.scanned++;
|
|
112
|
+
const existing = readValidity(rec.metadata);
|
|
113
|
+
const state = stateOfRecord(rec, now);
|
|
114
|
+
// 需要写回的两种情形:①从没标过;②state 变了(时间流逝把 current 熬成 expired 等)。
|
|
115
|
+
// validToSource 只在"从没标过"时按现算补;已标过的保留源头证据链(不就地升级证据等级)。
|
|
116
|
+
const needs = existing === undefined || existing.state !== state;
|
|
117
|
+
if (!needs)
|
|
118
|
+
continue;
|
|
119
|
+
const copy = { ...rec, metadata: { ...(rec.metadata ?? {}) } };
|
|
120
|
+
stampValidity(copy, now);
|
|
121
|
+
try {
|
|
122
|
+
if (l1.patchMetadata(copy.id, copy.metadata)) {
|
|
123
|
+
st.updated++;
|
|
124
|
+
dirty.push(copy.id);
|
|
125
|
+
if (dirty.length >= REPROJECT_BATCH) {
|
|
126
|
+
flushReproject(dirty.splice(0, dirty.length));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
st.failed++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
st.failed++;
|
|
135
|
+
logger.warn(`[memory] validity 回填单行失败(id=${copy.id},原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await yieldLoop();
|
|
139
|
+
}
|
|
140
|
+
flushReproject(dirty.splice(0, dirty.length));
|
|
141
|
+
if (!st.cancelled && hitCeil) {
|
|
142
|
+
const total = l1.size;
|
|
143
|
+
st.deferred = Math.max(0, total - st.scanned);
|
|
144
|
+
}
|
|
145
|
+
else if (st.cancelled) {
|
|
146
|
+
st.deferred = Math.max(0, l1.size - st.scanned);
|
|
147
|
+
}
|
|
148
|
+
st.running = false;
|
|
149
|
+
// 让最后一次状态可被面板读到:current 不在这里清,下轮 start 重置。
|
|
150
|
+
return { ...st };
|
|
151
|
+
}
|
|
152
|
+
/** 单行是否需要补标/重标(导出供单测钉判据;生产路径走 runValidityBackfillOnce)。 */
|
|
153
|
+
export function validityBackfillNeeds(metadata, record, now) {
|
|
154
|
+
const existing = readValidity(metadata);
|
|
155
|
+
if (existing === undefined)
|
|
156
|
+
return true;
|
|
157
|
+
return existing.state !== stateOfRecord(record, now);
|
|
158
|
+
}
|
|
159
|
+
/** 内部常量导出(测试钉预算/批形)。 */
|
|
160
|
+
export const VALIDITY_BACKFILL_TUNING = { PAGE, DEFAULT_MAX_RECORDS, DEFAULT_TIME_BUDGET_MS, REPROJECT_BATCH, VALIDITY_METADATA_KEY };
|
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 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
"host": "dist/index.js",
|
|
20
20
|
"client": "dist/client.js"
|
|
21
21
|
}
|
|
22
|
-
}
|
|
22
|
+
}
|
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",
|
|
@@ -152,4 +152,4 @@
|
|
|
152
152
|
"typescript-eslint": "^8.69.0",
|
|
153
153
|
"vitest": "^4.1.11"
|
|
154
154
|
}
|
|
155
|
-
}
|
|
155
|
+
}
|