dsh-prime-memory 0.25.0 → 0.26.1
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.en.md +37 -0
- package/CHANGELOG.ja.md +38 -0
- package/CHANGELOG.ko.md +38 -0
- package/CHANGELOG.md +39 -0
- package/dist/client.js +991 -665
- package/dist/config.d.ts +44 -2
- package/dist/config.js +32 -2
- package/dist/contract.d.ts +108 -2
- package/dist/index.js +3 -3
- package/dist/keyword-backfill.d.ts +52 -7
- package/dist/keyword-backfill.js +204 -62
- package/dist/llm.d.ts +9 -0
- package/dist/llm.js +7 -4
- package/dist/pipeline/relabel.d.ts +11 -12
- package/dist/pipeline/relabel.js +54 -66
- package/dist/pipeline/runner.js +24 -1
- package/dist/projection/recall.d.ts +3 -2
- package/dist/projection/recall.js +26 -5
- package/dist/projection/slots.d.ts +11 -0
- package/dist/projection/slots.js +13 -3
- package/dist/redistill-backfill.d.ts +148 -0
- package/dist/redistill-backfill.js +420 -0
- package/dist/room-review.d.ts +12 -13
- package/dist/room-review.js +39 -29
- package/dist/settings.js +15 -0
- package/dist/stats.js +138 -21
- package/dist/store/l1.d.ts +12 -0
- package/dist/store/l1.js +9 -0
- package/dist/store/memory-md.d.ts +8 -0
- package/dist/store/memory-md.js +13 -2
- package/dist/store/sqlite.d.ts +17 -0
- package/dist/store/sqlite.js +33 -0
- package/dist/tools/index.d.ts +10 -1
- package/dist/tools/index.js +7 -1
- package/dist/tools/skills.d.ts +7 -4
- package/dist/tools/skills.js +9 -11
- package/dist/wing-backfill.d.ts +25 -14
- package/dist/wing-backfill.js +133 -96
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/dist/wing-backfill.js
CHANGED
|
@@ -5,19 +5,51 @@ import { parseJsonLogged } from './llm.js';
|
|
|
5
5
|
import { isWingId, normTags } from './metadata-validators.js';
|
|
6
6
|
import { yieldLoop } from './util/yield.js';
|
|
7
7
|
/** 单次任务处理上限默认值(可被 cfg.backfill.maxRecords 覆盖;分批触发,可重复执行)。 */
|
|
8
|
-
const DEFAULT_MAX_RECORDS =
|
|
8
|
+
const DEFAULT_MAX_RECORDS = 40;
|
|
9
9
|
/** 墙钟预算默认值 ms(可被 cfg.backfill.timeBudgetMs 覆盖):超时剩留待再次触发。 */
|
|
10
|
-
const DEFAULT_TIME_BUDGET_MS =
|
|
11
|
-
/**
|
|
12
|
-
const
|
|
13
|
-
const state = { running: false, updated: 0, failed: 0 };
|
|
10
|
+
const DEFAULT_TIME_BUDGET_MS = 1_800_000;
|
|
11
|
+
/** 单次 LLM 请求默认超时(可被 cfg.backfill.callTimeoutMs 覆盖)。 */
|
|
12
|
+
const DEFAULT_CALL_TIMEOUT_MS = 240_000;
|
|
13
|
+
const state = { running: false, updated: 0, failed: 0, cancelled: false };
|
|
14
|
+
/**
|
|
15
|
+
* 人工中止信号。置位后:①不再取新记录;②在飞的那条先拿**宽限**(cancelGraceMs)
|
|
16
|
+
* 自然收工;③**宽限到期强杀**在飞请求——没有③的话「中止」最坏要等满 callTimeoutMs。
|
|
17
|
+
*/
|
|
18
|
+
let abortRequested = false;
|
|
19
|
+
/** 本轮在飞请求的强杀器(start 入口新建)。 */
|
|
20
|
+
let runAbort = null;
|
|
21
|
+
/** 中止宽限计时器(一次性)。 */
|
|
22
|
+
let graceTimer = null;
|
|
23
|
+
/** 本轮生效的中止宽限(启动时从 cfg 读定,已钳位)。 */
|
|
24
|
+
let graceMs = 15_000;
|
|
25
|
+
/**
|
|
26
|
+
* 中止宽限钳位。**硬界 30s**(用户定:中止必须在 30s 内强停)——无论 YAML 写了多少,
|
|
27
|
+
* 都不会超过这个值;未配置/非法取默认 15s。
|
|
28
|
+
*/
|
|
29
|
+
function clampGraceMs(raw) {
|
|
30
|
+
const n = typeof raw === 'number' && Number.isFinite(raw) ? Math.round(raw) : 15_000;
|
|
31
|
+
return Math.min(30_000, Math.max(0, n));
|
|
32
|
+
}
|
|
33
|
+
/** 请求中止本轮(空闲时无副作用);返回后中止是**有界**的:最多等 graceMs(≤30s)。 */
|
|
34
|
+
export function cancelWingBackfill() {
|
|
35
|
+
if (!state.running)
|
|
36
|
+
return { cancelled: false, running: false };
|
|
37
|
+
abortRequested = true;
|
|
38
|
+
if (graceTimer === null) {
|
|
39
|
+
graceTimer = setTimeout(() => {
|
|
40
|
+
graceTimer = null;
|
|
41
|
+
if (state.running)
|
|
42
|
+
runAbort?.abort();
|
|
43
|
+
}, Math.max(0, graceMs));
|
|
44
|
+
}
|
|
45
|
+
return { cancelled: true, running: true };
|
|
46
|
+
}
|
|
14
47
|
export function wingBackfillState() {
|
|
15
48
|
return { ...state };
|
|
16
49
|
}
|
|
17
|
-
const SYSTEM_PROMPT = '你是记忆库的 Wing
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'注意 id 必须原样照抄,不要改写、截断或改用序号。只输出 JSON,不要多余文字。';
|
|
50
|
+
const SYSTEM_PROMPT = '你是记忆库的 Wing 标注器。给你一条记忆,判断其内容属于哪个 Wing,输出 JSON 对象:{"wing":"<域id>"}。' +
|
|
51
|
+
'wing 只能从给定候选列表中选,且必须是列表里的原文 id;列表外的值一律无效(会被丢弃)。' +
|
|
52
|
+
'横跨多域或确实无法归入任何角时选 general。只输出 JSON,不要多余文字。';
|
|
21
53
|
/** 启动后台回填;已在运行返回 false(单飞)。 */
|
|
22
54
|
export function startWingBackfill(deps) {
|
|
23
55
|
if (state.running)
|
|
@@ -25,6 +57,13 @@ export function startWingBackfill(deps) {
|
|
|
25
57
|
state.running = true;
|
|
26
58
|
state.updated = 0;
|
|
27
59
|
state.failed = 0;
|
|
60
|
+
state.cancelled = false;
|
|
61
|
+
abortRequested = false;
|
|
62
|
+
if (graceTimer !== null) {
|
|
63
|
+
clearTimeout(graceTimer);
|
|
64
|
+
graceTimer = null;
|
|
65
|
+
}
|
|
66
|
+
runAbort = new AbortController();
|
|
28
67
|
void run(deps).finally(() => {
|
|
29
68
|
state.running = false;
|
|
30
69
|
deps.logger.info(`[memory] wing 回填结束:成功 ${state.updated} 条,失败 ${state.failed} 条`);
|
|
@@ -35,12 +74,14 @@ async function run(deps) {
|
|
|
35
74
|
const { ctx, cfg, backend, logger } = deps;
|
|
36
75
|
const MAX_RECORDS = cfg.backfill?.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
37
76
|
const TIME_BUDGET_MS = cfg.backfill?.timeBudgetMs ?? DEFAULT_TIME_BUDGET_MS;
|
|
77
|
+
graceMs = clampGraceMs(cfg.backfill?.cancelGraceMs);
|
|
78
|
+
const callTimeoutMs = cfg.backfill?.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
38
79
|
const halls = normWingEnabled(cfg.hall?.enabled);
|
|
39
80
|
if (halls.length === 0) {
|
|
40
81
|
logger.warn('[memory] wing 回填跳过:wing.enabled 为空(打标功能已关闭)');
|
|
41
82
|
return;
|
|
42
83
|
}
|
|
43
|
-
// 墙钟预算(cfg.backfill.timeBudgetMs
|
|
84
|
+
// 墙钟预算(cfg.backfill.timeBudgetMs):超时即停,剩余留待再次触发(任务本身设计为可重复执行)。
|
|
44
85
|
// 保证后台回填不长时间占据事件循环,设置面板的状态 RPC 始终优先。
|
|
45
86
|
const startedAt = Date.now();
|
|
46
87
|
const candidates = [...halls, WING_FALLBACK].join(' / ');
|
|
@@ -66,125 +107,121 @@ async function run(deps) {
|
|
|
66
107
|
}
|
|
67
108
|
await yieldLoop(); // 扫描批次间让位:面板状态 RPC 优先
|
|
68
109
|
}
|
|
69
|
-
|
|
110
|
+
const budget = Math.min(pendingIds.length, MAX_RECORDS);
|
|
70
111
|
logger.info(`[memory] wing 回填启动:未打标 ${pendingIds.length} 条,本次上限 ${budget} 条(候选 ${candidates})`);
|
|
71
|
-
|
|
112
|
+
// 逐条串行:取一条 → 送标 → 当场写回。不攒批、不合并(爆炸半径锁在 1 条)。
|
|
113
|
+
const ids = pendingIds.slice(0, budget);
|
|
114
|
+
for (let i = 0; i < ids.length; i++) {
|
|
115
|
+
if (abortRequested) {
|
|
116
|
+
state.cancelled = true;
|
|
117
|
+
logger.info(`[memory] wing 回填:已按请求中止,本轮完成 ${i} 条,剩 ${ids.length - i} 条留待再触发`);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
72
120
|
if (Date.now() - startedAt > TIME_BUDGET_MS) {
|
|
73
|
-
logger.info(`[memory] wing 回填:墙钟预算(${TIME_BUDGET_MS / 1000}s)用尽,剩余 ${
|
|
121
|
+
logger.info(`[memory] wing 回填:墙钟预算(${TIME_BUDGET_MS / 1000}s)用尽,剩余 ${ids.length - i} 条留待再次触发`);
|
|
74
122
|
break;
|
|
75
123
|
}
|
|
76
|
-
// 标注器需要 content 才能构造提示词,而扫描阶段只取了轻量投影 →
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (chunk.length === 0)
|
|
124
|
+
// 标注器需要 content 才能构造提示词,而扫描阶段只取了轻量投影 → 按 id 精确补水(主键索引)。
|
|
125
|
+
const [rec] = await backend.getByIds([ids[i]]);
|
|
126
|
+
if (!rec) {
|
|
127
|
+
state.failed++;
|
|
81
128
|
continue;
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
// 用 upsert 会把 content 写成空(数据静默丢失,已由 l1-paging 测试钉死)。
|
|
88
|
-
const meta = { ...(record.metadata ?? {}), hall: wing };
|
|
89
|
-
if (await backend.patchMetadata(record.id, meta))
|
|
90
|
-
state.updated++;
|
|
91
|
-
else
|
|
129
|
+
}
|
|
130
|
+
const wing = await labelWingOne(ctx, cfg, logger, rec, candidates, callTimeoutMs, runAbort?.signal);
|
|
131
|
+
if (wing === null) {
|
|
132
|
+
// 中止强杀导致的失败不计入 failed(不是模型/网络的错,是中止信号)
|
|
133
|
+
if (!abortRequested)
|
|
92
134
|
state.failed++;
|
|
93
|
-
|
|
94
|
-
await yieldLoop();
|
|
135
|
+
continue;
|
|
95
136
|
}
|
|
137
|
+
// 最小写回:只改 metadata(hall 是磁盘兼容键,不改名)。
|
|
138
|
+
// 这里**必须**用 patchMetadata 而非 upsert——巡检路径不持有正文,
|
|
139
|
+
// 用 upsert 会把 content 写成空(数据静默丢失,已由 l1-paging 测试钉死)。
|
|
140
|
+
const meta = { ...(rec.metadata ?? {}), hall: wing };
|
|
141
|
+
if (await backend.patchMetadata(rec.id, meta))
|
|
142
|
+
state.updated++;
|
|
143
|
+
else
|
|
144
|
+
state.failed++;
|
|
145
|
+
await yieldLoop(); // 写回间让位:面板状态 RPC 优先于后台逐条
|
|
96
146
|
}
|
|
97
147
|
}
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const user = `候选 Wing:${candidates}\n\n
|
|
148
|
+
/**
|
|
149
|
+
* Wing 标注器(导出):一键回填与反刍重标定共用同一 LLM 路径与措辞。
|
|
150
|
+
*
|
|
151
|
+
* **一个请求只装一条**(旧名 `labelWingChunk`,多条一次,已废)。返回该条的 wing,
|
|
152
|
+
* 失败/字段缺失/值非法返回 null(记账归调用方,本函数不动全局状态)。
|
|
153
|
+
*/
|
|
154
|
+
export async function labelWingOne(ctx, cfg, logger, rec, candidates, callTimeoutMs = DEFAULT_CALL_TIMEOUT_MS,
|
|
155
|
+
/** 本轮中止信号(可选):与单次超时**取或**,宽限到期后由它当场掐断请求。 */
|
|
156
|
+
externalSignal) {
|
|
157
|
+
const user = `候选 Wing:${candidates}\n\n记忆:\n${rec.content.slice(0, 600)}`;
|
|
108
158
|
try {
|
|
109
159
|
const raw = await callLLM(ctx, cfg, {
|
|
110
160
|
system: SYSTEM_PROMPT,
|
|
111
161
|
user,
|
|
112
|
-
maxTokens:
|
|
162
|
+
maxTokens: 1024,
|
|
113
163
|
layer: 'l1-extract',
|
|
164
|
+
// 打标是直答任务:显式关思考(不然思考模型把预算烧在 reasoning 上吐 0 字符)。
|
|
165
|
+
reasoningEffort: 'off',
|
|
114
166
|
logger,
|
|
167
|
+
signal: (() => {
|
|
168
|
+
const t = AbortSignal.timeout(callTimeoutMs > 0 ? callTimeoutMs : DEFAULT_CALL_TIMEOUT_MS);
|
|
169
|
+
return externalSignal ? AbortSignal.any([t, externalSignal]) : t;
|
|
170
|
+
})(),
|
|
115
171
|
});
|
|
116
172
|
const parsed = parseJsonLogged(raw, 'wing 回填', logger);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (rawWing === '') {
|
|
133
|
-
logger.warn(`[memory] wing 回填:返回项缺少 wing 字段,已丢弃(id=${record.id};实际键=${Object.keys(item ?? {}).join('|')})`);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
// 枚举校验:非法值不写入(含认知 hall 值如 facts/events —— 两者值域不相交)
|
|
137
|
-
if (!isWingId(rawWing)) {
|
|
138
|
-
logger.warn(`[memory] wing 回填:非法 wing 值「${rawWing.slice(0, 32)}」已丢弃(id=${record.id})`);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
out.push({ record, wing: rawWing });
|
|
173
|
+
// 通吃两种形状:模型偶尔无视“只输出一个对象”而回单元素数组(或带 id 的旧形状)。
|
|
174
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
175
|
+
// 字段名必须与提示词一致读 `wing`。历史上此处误读 `item.hall`(提示词早已是 wing),
|
|
176
|
+
// 导致整批静默丢弃——日志只增长 llmSkipped 而 wingLabeled 恒为 0,排查代价极高。
|
|
177
|
+
const rawWing = typeof obj?.wing === 'string' ? obj.wing.trim() : '';
|
|
178
|
+
// 字段缺失与值非法分开报:否则模型若又换了字段名,日志只会显示"非法值「」",
|
|
179
|
+
// 看不出真实原因是**读不到字段**——这正是上次事故难以定位的原因。
|
|
180
|
+
if (rawWing === '') {
|
|
181
|
+
logger.warn(`[memory] wing 回填:返回缺少 wing 字段,已丢弃(id=${rec.id};实际键=${Object.keys(obj ?? {}).join('|')})`);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
// 枚举校验:非法值不写入(含认知 hall 值如 facts/events —— 两者值域不相交)
|
|
185
|
+
if (!isWingId(rawWing)) {
|
|
186
|
+
logger.warn(`[memory] wing 回填:非法 wing 值「${rawWing.slice(0, 32)}」已丢弃(id=${rec.id})`);
|
|
187
|
+
return null;
|
|
142
188
|
}
|
|
143
|
-
return
|
|
189
|
+
return rawWing;
|
|
144
190
|
}
|
|
145
191
|
catch (err) {
|
|
146
|
-
logger.warn(`[memory] wing
|
|
147
|
-
|
|
148
|
-
return [];
|
|
192
|
+
logger.warn(`[memory] wing 回填单条失败(id=${rec.id},原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
+
return null;
|
|
149
194
|
}
|
|
150
195
|
}
|
|
151
|
-
const TAGGER_SYSTEM_PROMPT = '
|
|
196
|
+
const TAGGER_SYSTEM_PROMPT = '你是记忆库的标签标注器。给你一条记忆,为它提炼 1-3 个能概括其主题的英文 slug 标签' +
|
|
152
197
|
'(小写字母/数字/连字符,如 graphql-switch、riley-college-apps)。标签从内容中涌现,不从预定义列表选;' +
|
|
153
|
-
'输出 JSON
|
|
198
|
+
'输出 JSON 对象:{"tags":["slug1","slug2"]}。只输出 JSON,不要多余文字。';
|
|
154
199
|
/**
|
|
155
|
-
* 涌现标签标注器(导出)
|
|
156
|
-
*
|
|
200
|
+
* 涌现标签标注器(导出):为**一条**记录提炼 slug 标签(tags)——Room 的前身。
|
|
201
|
+
* 一个请求只装一条(旧名 `tagChunk`,多条一次,已废)。失败返回 null;合法但为空返回 []。
|
|
157
202
|
*/
|
|
158
|
-
export async function
|
|
159
|
-
const
|
|
203
|
+
export async function tagOne(ctx, cfg, logger, rec, callTimeoutMs = DEFAULT_CALL_TIMEOUT_MS) {
|
|
204
|
+
const user = `记忆:\n${rec.content.slice(0, 600)}`;
|
|
160
205
|
try {
|
|
161
206
|
const raw = await callLLM(ctx, cfg, {
|
|
162
207
|
system: TAGGER_SYSTEM_PROMPT,
|
|
163
|
-
user
|
|
164
|
-
maxTokens:
|
|
208
|
+
user,
|
|
209
|
+
maxTokens: 1024,
|
|
165
210
|
layer: 'l1-extract',
|
|
211
|
+
// 同 wing 标:直答任务,显式关思考。
|
|
212
|
+
reasoningEffort: 'off',
|
|
166
213
|
logger,
|
|
214
|
+
signal: AbortSignal.timeout(callTimeoutMs > 0 ? callTimeoutMs : DEFAULT_CALL_TIMEOUT_MS),
|
|
167
215
|
});
|
|
168
216
|
const parsed = parseJsonLogged(raw, 'tags 标注', logger);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const record = typeof item?.id === 'string' ? byId.get(item.id) : undefined;
|
|
175
|
-
if (!record) {
|
|
176
|
-
logger.warn(`[memory] tags 标注:id 配对失败,已丢弃(id=${String(item?.id).slice(0, 40)})`);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
// 归一 + 校验在标注器内部就做一遍(写回点仍会再过一次,双保险)
|
|
180
|
-
const tags = normTags(item?.tags);
|
|
181
|
-
if (tags.length > 0)
|
|
182
|
-
out.push({ record, tags });
|
|
183
|
-
}
|
|
184
|
-
return out;
|
|
217
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
218
|
+
if (!obj || typeof obj !== 'object')
|
|
219
|
+
return null;
|
|
220
|
+
// 归一 + 校验在标注器内部就做一遍(写回点仍会再过一次,双保险)
|
|
221
|
+
return normTags(obj.tags);
|
|
185
222
|
}
|
|
186
223
|
catch (err) {
|
|
187
|
-
logger.warn(`[memory] tags
|
|
188
|
-
return
|
|
224
|
+
logger.warn(`[memory] tags 标注单条失败(id=${rec.id},原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
225
|
+
return null;
|
|
189
226
|
}
|
|
190
227
|
}
|
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.
|
|
4
|
+
"version": "0.26.1",
|
|
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 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|