dsh-prime-memory 0.24.2 → 0.26.0
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 +28 -0
- package/CHANGELOG.ja.md +29 -0
- package/CHANGELOG.ko.md +29 -0
- package/CHANGELOG.md +30 -0
- package/dist/client.js +907 -654
- package/dist/config.d.ts +47 -2
- package/dist/config.js +30 -2
- package/dist/contract.d.ts +107 -1
- package/dist/keyword-backfill.d.ts +75 -0
- package/dist/keyword-backfill.js +218 -0
- package/dist/metadata-validators.d.ts +9 -0
- package/dist/metadata-validators.js +31 -0
- 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 +133 -0
- package/dist/redistill-backfill.js +352 -0
- package/dist/room-review.d.ts +12 -13
- package/dist/room-review.js +37 -29
- package/dist/settings.js +13 -0
- package/dist/stats.js +126 -14
- package/dist/store/l1.d.ts +18 -0
- package/dist/store/l1.js +17 -0
- package/dist/store/memory-backend-worker.d.ts +1 -0
- package/dist/store/memory-backend-worker.js +3 -0
- package/dist/store/memory-backend.d.ts +3 -0
- package/dist/store/memory-backend.js +3 -0
- package/dist/store/memory-md.d.ts +8 -0
- package/dist/store/memory-md.js +13 -2
- package/dist/store/memory-worker.js +3 -0
- package/dist/store/sqlite.d.ts +28 -0
- package/dist/store/sqlite.js +66 -0
- package/dist/wing-backfill.d.ts +16 -14
- package/dist/wing-backfill.js +76 -96
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/dist/wing-backfill.js
CHANGED
|
@@ -5,19 +5,18 @@ 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
|
|
10
|
+
const DEFAULT_TIME_BUDGET_MS = 1_800_000;
|
|
11
|
+
/** 单次 LLM 请求默认超时(可被 cfg.backfill.callTimeoutMs 覆盖)。 */
|
|
12
|
+
const DEFAULT_CALL_TIMEOUT_MS = 240_000;
|
|
13
13
|
const state = { running: false, updated: 0, failed: 0 };
|
|
14
14
|
export function wingBackfillState() {
|
|
15
15
|
return { ...state };
|
|
16
16
|
}
|
|
17
|
-
const SYSTEM_PROMPT = '你是记忆库的 Wing
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'注意 id 必须原样照抄,不要改写、截断或改用序号。只输出 JSON,不要多余文字。';
|
|
17
|
+
const SYSTEM_PROMPT = '你是记忆库的 Wing 标注器。给你一条记忆,判断其内容属于哪个 Wing,输出 JSON 对象:{"wing":"<域id>"}。' +
|
|
18
|
+
'wing 只能从给定候选列表中选,且必须是列表里的原文 id;列表外的值一律无效(会被丢弃)。' +
|
|
19
|
+
'横跨多域或确实无法归入任何角时选 general。只输出 JSON,不要多余文字。';
|
|
21
20
|
/** 启动后台回填;已在运行返回 false(单飞)。 */
|
|
22
21
|
export function startWingBackfill(deps) {
|
|
23
22
|
if (state.running)
|
|
@@ -35,12 +34,13 @@ async function run(deps) {
|
|
|
35
34
|
const { ctx, cfg, backend, logger } = deps;
|
|
36
35
|
const MAX_RECORDS = cfg.backfill?.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
37
36
|
const TIME_BUDGET_MS = cfg.backfill?.timeBudgetMs ?? DEFAULT_TIME_BUDGET_MS;
|
|
37
|
+
const callTimeoutMs = cfg.backfill?.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
38
38
|
const halls = normWingEnabled(cfg.hall?.enabled);
|
|
39
39
|
if (halls.length === 0) {
|
|
40
40
|
logger.warn('[memory] wing 回填跳过:wing.enabled 为空(打标功能已关闭)');
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
-
// 墙钟预算(cfg.backfill.timeBudgetMs
|
|
43
|
+
// 墙钟预算(cfg.backfill.timeBudgetMs):超时即停,剩余留待再次触发(任务本身设计为可重复执行)。
|
|
44
44
|
// 保证后台回填不长时间占据事件循环,设置面板的状态 RPC 始终优先。
|
|
45
45
|
const startedAt = Date.now();
|
|
46
46
|
const candidates = [...halls, WING_FALLBACK].join(' / ');
|
|
@@ -66,125 +66,105 @@ async function run(deps) {
|
|
|
66
66
|
}
|
|
67
67
|
await yieldLoop(); // 扫描批次间让位:面板状态 RPC 优先
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
const budget = Math.min(pendingIds.length, MAX_RECORDS);
|
|
70
70
|
logger.info(`[memory] wing 回填启动:未打标 ${pendingIds.length} 条,本次上限 ${budget} 条(候选 ${candidates})`);
|
|
71
|
-
|
|
71
|
+
// 逐条串行:取一条 → 送标 → 当场写回。不攒批、不合并(爆炸半径锁在 1 条)。
|
|
72
|
+
const ids = pendingIds.slice(0, budget);
|
|
73
|
+
for (let i = 0; i < ids.length; i++) {
|
|
72
74
|
if (Date.now() - startedAt > TIME_BUDGET_MS) {
|
|
73
|
-
logger.info(`[memory] wing 回填:墙钟预算(${TIME_BUDGET_MS / 1000}s)用尽,剩余 ${
|
|
75
|
+
logger.info(`[memory] wing 回填:墙钟预算(${TIME_BUDGET_MS / 1000}s)用尽,剩余 ${ids.length - i} 条留待再次触发`);
|
|
74
76
|
break;
|
|
75
77
|
}
|
|
76
|
-
// 标注器需要 content 才能构造提示词,而扫描阶段只取了轻量投影 →
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (chunk.length === 0)
|
|
78
|
+
// 标注器需要 content 才能构造提示词,而扫描阶段只取了轻量投影 → 按 id 精确补水(主键索引)。
|
|
79
|
+
const [rec] = await backend.getByIds([ids[i]]);
|
|
80
|
+
if (!rec) {
|
|
81
|
+
state.failed++;
|
|
81
82
|
continue;
|
|
82
|
-
budget -= chunk.length;
|
|
83
|
-
const labeled = await labelWingChunk(ctx, cfg, logger, chunk, candidates);
|
|
84
|
-
for (const { record, wing } of labeled) {
|
|
85
|
-
// 最小写回:只改 metadata(hall 是磁盘兼容键,不改名)。
|
|
86
|
-
// 这里**必须**用 patchMetadata 而非 upsert——批量巡检路径不持有正文,
|
|
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
|
|
92
|
-
state.failed++;
|
|
93
|
-
// 写回间让位:面板状态 RPC 优先于后台批次
|
|
94
|
-
await yieldLoop();
|
|
95
83
|
}
|
|
84
|
+
const wing = await labelWingOne(ctx, cfg, logger, rec, candidates, callTimeoutMs);
|
|
85
|
+
if (wing === null) {
|
|
86
|
+
state.failed++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// 最小写回:只改 metadata(hall 是磁盘兼容键,不改名)。
|
|
90
|
+
// 这里**必须**用 patchMetadata 而非 upsert——巡检路径不持有正文,
|
|
91
|
+
// 用 upsert 会把 content 写成空(数据静默丢失,已由 l1-paging 测试钉死)。
|
|
92
|
+
const meta = { ...(rec.metadata ?? {}), hall: wing };
|
|
93
|
+
if (await backend.patchMetadata(rec.id, meta))
|
|
94
|
+
state.updated++;
|
|
95
|
+
else
|
|
96
|
+
state.failed++;
|
|
97
|
+
await yieldLoop(); // 写回间让位:面板状态 RPC 优先于后台逐条
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const list = chunk.map((r, i) => `${i + 1}. id=${r.id}\n${r.content.slice(0, 300)}`).join('\n\n');
|
|
107
|
-
const user = `候选 Wing:${candidates}\n\n记忆列表:\n${list}`;
|
|
100
|
+
/**
|
|
101
|
+
* Wing 标注器(导出):一键回填与反刍重标定共用同一 LLM 路径与措辞。
|
|
102
|
+
*
|
|
103
|
+
* **一个请求只装一条**(旧名 `labelWingChunk`,多条一次,已废)。返回该条的 wing,
|
|
104
|
+
* 失败/字段缺失/值非法返回 null(记账归调用方,本函数不动全局状态)。
|
|
105
|
+
*/
|
|
106
|
+
export async function labelWingOne(ctx, cfg, logger, rec, candidates, callTimeoutMs = DEFAULT_CALL_TIMEOUT_MS) {
|
|
107
|
+
const user = `候选 Wing:${candidates}\n\n记忆:\n${rec.content.slice(0, 600)}`;
|
|
108
108
|
try {
|
|
109
109
|
const raw = await callLLM(ctx, cfg, {
|
|
110
110
|
system: SYSTEM_PROMPT,
|
|
111
111
|
user,
|
|
112
|
-
maxTokens:
|
|
112
|
+
maxTokens: 1024,
|
|
113
113
|
layer: 'l1-extract',
|
|
114
114
|
logger,
|
|
115
|
+
signal: AbortSignal.timeout(callTimeoutMs > 0 ? callTimeoutMs : DEFAULT_CALL_TIMEOUT_MS),
|
|
115
116
|
});
|
|
116
117
|
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 });
|
|
118
|
+
// 通吃两种形状:模型偶尔无视“只输出一个对象”而回单元素数组(或带 id 的旧形状)。
|
|
119
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
120
|
+
// 字段名必须与提示词一致读 `wing`。历史上此处误读 `item.hall`(提示词早已是 wing),
|
|
121
|
+
// 导致整批静默丢弃——日志只增长 llmSkipped 而 wingLabeled 恒为 0,排查代价极高。
|
|
122
|
+
const rawWing = typeof obj?.wing === 'string' ? obj.wing.trim() : '';
|
|
123
|
+
// 字段缺失与值非法分开报:否则模型若又换了字段名,日志只会显示"非法值「」",
|
|
124
|
+
// 看不出真实原因是**读不到字段**——这正是上次事故难以定位的原因。
|
|
125
|
+
if (rawWing === '') {
|
|
126
|
+
logger.warn(`[memory] wing 回填:返回缺少 wing 字段,已丢弃(id=${rec.id};实际键=${Object.keys(obj ?? {}).join('|')})`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// 枚举校验:非法值不写入(含认知 hall 值如 facts/events —— 两者值域不相交)
|
|
130
|
+
if (!isWingId(rawWing)) {
|
|
131
|
+
logger.warn(`[memory] wing 回填:非法 wing 值「${rawWing.slice(0, 32)}」已丢弃(id=${rec.id})`);
|
|
132
|
+
return null;
|
|
142
133
|
}
|
|
143
|
-
return
|
|
134
|
+
return rawWing;
|
|
144
135
|
}
|
|
145
136
|
catch (err) {
|
|
146
|
-
logger.warn(`[memory] wing
|
|
147
|
-
|
|
148
|
-
return [];
|
|
137
|
+
logger.warn(`[memory] wing 回填单条失败(id=${rec.id},原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
138
|
+
return null;
|
|
149
139
|
}
|
|
150
140
|
}
|
|
151
|
-
const TAGGER_SYSTEM_PROMPT = '
|
|
141
|
+
const TAGGER_SYSTEM_PROMPT = '你是记忆库的标签标注器。给你一条记忆,为它提炼 1-3 个能概括其主题的英文 slug 标签' +
|
|
152
142
|
'(小写字母/数字/连字符,如 graphql-switch、riley-college-apps)。标签从内容中涌现,不从预定义列表选;' +
|
|
153
|
-
'输出 JSON
|
|
143
|
+
'输出 JSON 对象:{"tags":["slug1","slug2"]}。只输出 JSON,不要多余文字。';
|
|
154
144
|
/**
|
|
155
|
-
* 涌现标签标注器(导出)
|
|
156
|
-
*
|
|
145
|
+
* 涌现标签标注器(导出):为**一条**记录提炼 slug 标签(tags)——Room 的前身。
|
|
146
|
+
* 一个请求只装一条(旧名 `tagChunk`,多条一次,已废)。失败返回 null;合法但为空返回 []。
|
|
157
147
|
*/
|
|
158
|
-
export async function
|
|
159
|
-
const
|
|
148
|
+
export async function tagOne(ctx, cfg, logger, rec, callTimeoutMs = DEFAULT_CALL_TIMEOUT_MS) {
|
|
149
|
+
const user = `记忆:\n${rec.content.slice(0, 600)}`;
|
|
160
150
|
try {
|
|
161
151
|
const raw = await callLLM(ctx, cfg, {
|
|
162
152
|
system: TAGGER_SYSTEM_PROMPT,
|
|
163
|
-
user
|
|
164
|
-
maxTokens:
|
|
153
|
+
user,
|
|
154
|
+
maxTokens: 1024,
|
|
165
155
|
layer: 'l1-extract',
|
|
166
156
|
logger,
|
|
157
|
+
signal: AbortSignal.timeout(callTimeoutMs > 0 ? callTimeoutMs : DEFAULT_CALL_TIMEOUT_MS),
|
|
167
158
|
});
|
|
168
159
|
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;
|
|
160
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
161
|
+
if (!obj || typeof obj !== 'object')
|
|
162
|
+
return null;
|
|
163
|
+
// 归一 + 校验在标注器内部就做一遍(写回点仍会再过一次,双保险)
|
|
164
|
+
return normTags(obj.tags);
|
|
185
165
|
}
|
|
186
166
|
catch (err) {
|
|
187
|
-
logger.warn(`[memory] tags
|
|
188
|
-
return
|
|
167
|
+
logger.warn(`[memory] tags 标注单条失败(id=${rec.id},原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
+
return null;
|
|
189
169
|
}
|
|
190
170
|
}
|
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.0",
|
|
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 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|