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.
Files changed (40) hide show
  1. package/CHANGELOG.en.md +28 -0
  2. package/CHANGELOG.ja.md +29 -0
  3. package/CHANGELOG.ko.md +29 -0
  4. package/CHANGELOG.md +30 -0
  5. package/dist/client.js +907 -654
  6. package/dist/config.d.ts +47 -2
  7. package/dist/config.js +30 -2
  8. package/dist/contract.d.ts +107 -1
  9. package/dist/keyword-backfill.d.ts +75 -0
  10. package/dist/keyword-backfill.js +218 -0
  11. package/dist/metadata-validators.d.ts +9 -0
  12. package/dist/metadata-validators.js +31 -0
  13. package/dist/pipeline/relabel.d.ts +11 -12
  14. package/dist/pipeline/relabel.js +54 -66
  15. package/dist/pipeline/runner.js +24 -1
  16. package/dist/projection/recall.d.ts +3 -2
  17. package/dist/projection/recall.js +26 -5
  18. package/dist/projection/slots.d.ts +11 -0
  19. package/dist/projection/slots.js +13 -3
  20. package/dist/redistill-backfill.d.ts +133 -0
  21. package/dist/redistill-backfill.js +352 -0
  22. package/dist/room-review.d.ts +12 -13
  23. package/dist/room-review.js +37 -29
  24. package/dist/settings.js +13 -0
  25. package/dist/stats.js +126 -14
  26. package/dist/store/l1.d.ts +18 -0
  27. package/dist/store/l1.js +17 -0
  28. package/dist/store/memory-backend-worker.d.ts +1 -0
  29. package/dist/store/memory-backend-worker.js +3 -0
  30. package/dist/store/memory-backend.d.ts +3 -0
  31. package/dist/store/memory-backend.js +3 -0
  32. package/dist/store/memory-md.d.ts +8 -0
  33. package/dist/store/memory-md.js +13 -2
  34. package/dist/store/memory-worker.js +3 -0
  35. package/dist/store/sqlite.d.ts +28 -0
  36. package/dist/store/sqlite.js +66 -0
  37. package/dist/wing-backfill.d.ts +16 -14
  38. package/dist/wing-backfill.js +76 -96
  39. package/dsh.plugin.json +1 -1
  40. package/package.json +1 -1
@@ -5,9 +5,12 @@
5
5
  * - Wing 合法性:metadata.hall 非空但不在词表(8 角 + general)→ 剥离非法值,转入 LLM 重标队列;
6
6
  * - 认知 hall:按 type 静态映射(cognitive-hall.ts)可派生而 metadata.cogHall 缺失/不一致 → 补写修正;
7
7
  * 2. **LLM 重标定**(有界批,受 wing.enabled 门控):
8
- * - 未打标 wing 的记录复用一键回填的标注器(labelWingChunk)补打;
8
+ * - 未打标 wing 的记录复用一键回填的标注器(labelWingOne)补打;
9
9
  * - 涌现标签(tags,Room 的前身):同一批记录由标注器顺带产出 1-3 个 slug 标签写 metadata.tags。
10
10
  *
11
+ * 形状约定(用户定):**按单元、串行、不合并** —— 一个请求只装一条记录
12
+ * (旧形状 20 条/请求,一次截断/空输出就带走整批)。
13
+ *
11
14
  * 安全边界与一键回填一致:LLM 失败只跳过当前块、原记录零改动;机械与 LLM 写回都有上限,防止单次跑飞。
12
15
  * 本阶段在反刍收尾(L2/L3 之后)执行,**任何失败不拖垮反刍整体**(只 warn + 计数)。
13
16
  */
@@ -38,27 +41,23 @@ export interface RelabelDeps {
38
41
  backend: MemoryBackend;
39
42
  logger: MemoryLogger;
40
43
  }
41
- /** 测试注入口:分别替换 wing 标注器与 tags 标注器(默认走真实 LLM 路径)。 */
44
+ /** 测试注入口:分别替换 wing 标注器与 tags 标注器(默认走真实 LLM 路径)。
45
+ * 两个标注器都是**逐条**形状(一个请求一条):成功返回结果,失败返回 null。 */
42
46
  export interface RelabelOverrides {
43
- wingLabeler?: (chunk: MemoryRecord[]) => Promise<Array<{
44
- record: MemoryRecord;
45
- wing: string;
46
- }>>;
47
- tagger?: (chunk: MemoryRecord[]) => Promise<Array<{
48
- record: MemoryRecord;
49
- tags: string[];
50
- }>>;
47
+ wingLabeler?: (rec: MemoryRecord) => Promise<string | null>;
48
+ tagger?: (rec: MemoryRecord) => Promise<string[] | null>;
51
49
  }
52
50
  export interface RelabelOpts {
53
51
  /**
54
52
  * 批次进度回调(relabeling 阶段的 detail/子进度由此驱动)。
55
53
  *
56
54
  * 第 4 参 `label` 用于**区分机械段与 LLM 段**:机械巡检按 200 条/批推进,
57
- * LLM 段按 20 条/批推进,两者粒度不同,面板必须能分辨(否则用户看到的是
55
+ * LLM 段**逐条**推进,两者粒度不同,面板必须能分辨(否则用户看到的是
58
56
  * 一个忽快忽慢的"重标定批次")。
59
57
  */
60
58
  progress?: (text: string, done: number, total: number, label?: string) => void;
61
- /** LLM 段墙钟预算(毫秒);超时停止,剩余计入 deferred 留待下次反刍。默认 90s。 */
59
+ /** LLM 段墙钟预算(毫秒);超时停止,剩余计入 deferred 留待下次反刍。默认 180s(逐条形状下
60
+ * 约 60 条 × 2.6s 的余量)。 */
62
61
  timeBudgetMs?: number;
63
62
  }
64
63
  export declare function relabelPass(deps: RelabelDeps, overrides?: RelabelOverrides, opts?: RelabelOpts): Promise<RelabelStats>;
@@ -1,18 +1,16 @@
1
1
  import { cognitiveHallOf, COG_HALL_METADATA_KEY } from '../cognitive-hall.js';
2
2
  import { normWingEnabled } from '../config.js';
3
3
  import { WING_FALLBACK } from '../types.js';
4
- import { labelWingChunk, tagChunk } from '../wing-backfill.js';
4
+ import { labelWingOne, tagOne } from '../wing-backfill.js';
5
5
  import { isWingId, normTags } from '../metadata-validators.js';
6
6
  import { yieldLoop } from '../util/yield.js';
7
7
  /** 机械写回上限(首次全量巡检可能上千条待补 cogHall;分次反刍消化,防单次跑飞)。 */
8
8
  const MECH_CAP = 800;
9
- /** LLM 重标定批上限(每块 20 条由标注器内部控制)。 */
9
+ /** LLM 重标定批上限(本次最多处理多少条待重标记录)。 */
10
10
  const LLM_CAP = 60;
11
- /** LLM 批大小(一次调用判定的记录数;批次进度/预算/让位都以此为粒度)。 */
12
- const LLM_CHUNK = 20;
13
11
  export async function relabelPass(deps, overrides = {}, opts = {}) {
14
12
  const { ctx, cfg, backend, logger } = deps;
15
- const timeBudgetMs = opts.timeBudgetMs ?? 90_000;
13
+ const timeBudgetMs = opts.timeBudgetMs ?? 180_000;
16
14
  const stats = {
17
15
  checked: 0,
18
16
  cogHallFixed: 0,
@@ -106,95 +104,85 @@ export async function relabelPass(deps, overrides = {}, opts = {}) {
106
104
  return stats;
107
105
  logger.info(`[memory] 反刍重标定:未打标/待重标 ${needWingIds.length} 条,本次 LLM 处理 ${batch.length} 条(候选 ${[...wingEnabled, WING_FALLBACK].join(' / ')})`);
108
106
  const wingLabeler = overrides.wingLabeler ??
109
- ((chunk) => labelWingChunk(ctx, cfg, logger, chunk, [...wingEnabled, WING_FALLBACK].join(' / ')));
110
- // ── LLM wing 重标定:20 条/批逐批推进,批间让位 + 墙钟预算 + 批次进度回显 ──
107
+ ((rec) => labelWingOne(ctx, cfg, logger, rec, [...wingEnabled, WING_FALLBACK].join(' / ')));
108
+ // ── LLM wing 重标定:逐条推进,条间让位 + 墙钟预算 + 条序进度回显 ──
111
109
  const startedAt = Date.now();
112
110
  const overBudget = () => Date.now() - startedAt > timeBudgetMs;
113
- const chunks = [];
114
- for (let i = 0; i < batch.length; i += LLM_CHUNK)
115
- chunks.push(batch.slice(i, i + LLM_CHUNK));
116
111
  let doneCount = 0;
117
112
  const taggedPool = [];
118
- for (let i = 0; i < chunks.length; i++) {
113
+ for (let i = 0; i < batch.length; i++) {
119
114
  if (overBudget()) {
120
115
  stats.deferred += batch.length - doneCount;
121
116
  opts.progress?.(`重标定:时间预算(${Math.round(timeBudgetMs / 1000)}s)用尽,剩余 ${batch.length - doneCount} 条留待下次反刍`, doneCount, batch.length);
122
117
  logger.info(`[memory] 反刍重标定:时间预算用尽,deferred ${stats.deferred} 条`);
123
118
  return stats;
124
119
  }
125
- opts.progress?.(`重标定:LLM 补 wing 第 ${i + 1}/${chunks.length} 批(已完成 ${doneCount}/${batch.length} 条)`, doneCount, batch.length, '补 Wing');
126
- let rows;
120
+ opts.progress?.(`重标定:LLM 补 wing 第 ${i + 1}/${batch.length} 条(已完成 ${doneCount}/${batch.length} 条)`, doneCount, batch.length, '补 Wing');
121
+ let wing;
127
122
  try {
128
- rows = await wingLabeler(chunks[i]);
123
+ wing = await wingLabeler(batch[i]);
129
124
  }
130
125
  catch (err) {
131
- // labelWingChunk 自身已兜底(不向调用方抛);能走到这里只可能是注入桩抛错。
126
+ // labelWingOne 自身已兜底(不向调用方抛);能走到这里只可能是注入桩抛错。
132
127
  // 即便如此也必须留痕——静默吞掉批次是本次「整批丢弃却无日志」的直接教训。
133
- stats.llmSkipped += chunks[i].length;
134
- doneCount += chunks[i].length;
135
- logger.warn(`[memory] 重标定第 ${i + 1}/${chunks.length} 批标注器抛错,跳过 ${chunks[i].length} 条: ${err instanceof Error ? err.message : String(err)}`);
128
+ stats.llmSkipped++;
129
+ doneCount++;
130
+ logger.warn(`[memory] 重标定第 ${i + 1}/${batch.length} 条标注器抛错,跳过: ${err instanceof Error ? err.message : String(err)}`);
136
131
  continue;
137
132
  }
138
- const dropped = chunks[i].length - rows.length;
139
- if (dropped > 0) {
140
- logger.warn(`[memory] 重标定第 ${i + 1}/${chunks.length} 批丢弃 ${dropped} 条(详见上方 wing 回填日志)`);
133
+ if (wing === null) {
134
+ // 标注器已 warn(字段缺失/非法值/单条失败),这里只记账。
135
+ stats.llmSkipped++;
136
+ doneCount++;
137
+ continue;
141
138
  }
142
- stats.llmSkipped += dropped;
143
- stats.wingLabeled += rows.length;
144
139
  // 写前重读:机械阶段可能已写过 cogHall,标注器持有旧副本——以库内最新为基线合并
145
- const freshRows = await backend.getByIds(rows.map(({ record }) => record.id));
146
- const freshById = new Map(freshRows.map((r) => [r.id, r]));
147
- for (const { record, wing } of rows) {
148
- const current = freshById.get(record.id) ?? record;
149
- const meta = { ...(current.metadata ?? {}) };
150
- meta.hall = wing;
151
- try {
152
- await backend.patchMetadata(current.id, meta);
153
- taggedPool.push(current);
154
- }
155
- catch {
156
- stats.wingLabeled--;
157
- stats.llmSkipped++;
158
- }
159
- await yieldLoop();
140
+ const [fresh] = await backend.getByIds([batch[i].id]);
141
+ const current = fresh ?? batch[i];
142
+ const meta = { ...(current.metadata ?? {}) };
143
+ meta.hall = wing;
144
+ try {
145
+ await backend.patchMetadata(current.id, meta);
146
+ stats.wingLabeled++;
147
+ taggedPool.push(current);
148
+ }
149
+ catch {
150
+ stats.llmSkipped++;
160
151
  }
161
- doneCount += chunks[i].length;
152
+ doneCount++;
153
+ await yieldLoop();
162
154
  }
163
- // ── 涌现标签(tags,Room 前身):同批有界 40 条,同样分批 + 预算 + 进度 ──
155
+ // ── 涌现标签(tags,Room 前身):同批有界 40 条,同样逐条 + 预算 + 进度 ──
164
156
  const tagBatch = taggedPool.slice(0, 40);
165
- const tagger = overrides.tagger ?? ((chunk) => tagChunk(ctx, cfg, logger, chunk));
166
- const tagChunks = [];
167
- for (let i = 0; i < tagBatch.length; i += LLM_CHUNK)
168
- tagChunks.push(tagBatch.slice(i, i + LLM_CHUNK));
169
- for (let i = 0; i < tagChunks.length; i++) {
157
+ const tagger = overrides.tagger ?? ((rec) => tagOne(ctx, cfg, logger, rec));
158
+ for (let i = 0; i < tagBatch.length; i++) {
170
159
  if (overBudget())
171
160
  break;
172
- opts.progress?.(`重标定:提炼涌现标签 第 ${i + 1}/${tagChunks.length} 批`, doneCount, batch.length, '提炼标签');
173
- let tagRows;
161
+ opts.progress?.(`重标定:提炼涌现标签 第 ${i + 1}/${tagBatch.length} 条`, doneCount, batch.length, '提炼标签');
162
+ let rawTags;
174
163
  try {
175
- tagRows = await tagger(tagChunks[i]);
164
+ rawTags = await tagger(tagBatch[i]);
176
165
  }
177
166
  catch (err) {
178
- logger.warn(`[memory] 重标定 tags 第 ${i + 1}/${tagChunks.length} 批标注器抛错,跳过 ${tagChunks[i].length} 条: ${err instanceof Error ? err.message : String(err)}`);
167
+ logger.warn(`[memory] 重标定 tags 第 ${i + 1}/${tagBatch.length} 条标注器抛错,跳过: ${err instanceof Error ? err.message : String(err)}`);
179
168
  continue;
180
169
  }
181
- const freshTagRows = await backend.getByIds(tagRows.map(({ record }) => record.id));
182
- const freshForTags = new Map(freshTagRows.map((r) => [r.id, r]));
183
- for (const { record, tags: rawTags } of tagRows) {
184
- const tags = normTags(rawTags); // 归一+校验在写回点强制:无论标签来自真实 LLM 还是注入桩
185
- if (tags.length === 0)
186
- continue;
187
- const current = freshForTags.get(record.id) ?? record;
188
- const meta = { ...(current.metadata ?? {}) };
189
- meta.tags = tags;
190
- try {
191
- await backend.patchMetadata(current.id, meta);
192
- stats.tagged++;
193
- await yieldLoop();
194
- }
195
- catch {
196
- /* 标签写失败不影响主流程 */
197
- }
170
+ if (!rawTags)
171
+ continue;
172
+ const tags = normTags(rawTags); // 归一+校验在写回点强制:无论标签来自真实 LLM 还是注入桩
173
+ if (tags.length === 0)
174
+ continue;
175
+ const [fresh] = await backend.getByIds([tagBatch[i].id]);
176
+ const current = fresh ?? tagBatch[i];
177
+ const meta = { ...(current.metadata ?? {}) };
178
+ meta.tags = tags;
179
+ try {
180
+ await backend.patchMetadata(current.id, meta);
181
+ stats.tagged++;
182
+ await yieldLoop();
183
+ }
184
+ catch {
185
+ /* 标签写失败不影响主流程 */
198
186
  }
199
187
  }
200
188
  return stats;
@@ -121,17 +121,40 @@ export function effectiveCfg(cfg, live) {
121
121
  const cfOverride = s?.conflictFreeze !== undefined
122
122
  ? { conflictFreeze: { ...(cfg.conflictFreeze ?? {}), enabled: s.conflictFreeze } }
123
123
  : null;
124
+ // 存量回填/进阶功能启用开关:live 与部署 cfg.<x>.enabled 取或(部署强制开、live 追加开,
125
+ // 任一为真即放行)。关关关时保持逐字 false(零漂移不破)。
126
+ const kwOverride = s?.keywordEnabled !== undefined
127
+ ? { keyword: { ...(cfg.keyword ?? {}), enabled: cfg.keyword?.enabled === true || s.keywordEnabled === true } }
128
+ : null;
129
+ const rdOverride = s?.redistillEnabled !== undefined
130
+ ? { redistill: { ...(cfg.redistill ?? {}), enabled: cfg.redistill?.enabled === true || s.redistillEnabled === true } }
131
+ : null;
132
+ const mdsOverride = s?.mdSyncEnabled !== undefined
133
+ ? { mdSync: { ...(cfg.mdSync ?? {}), enabled: cfg.mdSync?.enabled === true || s.mdSyncEnabled === true } }
134
+ : null;
135
+ const mpOverride = s?.memportEnabled !== undefined
136
+ ? { memport: { ...(cfg.memport ?? {}), enabled: cfg.memport?.enabled === true || s.memportEnabled === true } }
137
+ : null;
124
138
  // md 投射/后缀收敛的单次动作护栏运行时覆盖(0 = 跟随部署 cfg.memExport.maxRecords)。
125
139
  // 只覆盖条数护栏,不动 enabled/root——开关与根路径仍属部署配置。
126
140
  const mdx = s?.mdExportMaxRecords ?? 0;
127
141
  const memExportOverride = mdx > 0 ? { memExport: { ...(cfg.memExport ?? {}), maxRecords: mdx } } : null;
128
- if (!override && !budgets && !maxInput && !fallbacksTakeover && !chainEffort && !chainFallbacks && !effortInject && !layerChains && !hasChannel && !embedOverride && !cfOverride && !memExportOverride)
142
+ // 回填并行度运行时覆盖(0 = 跟随部署 cfg.backfill.concurrency):只改并发度,
143
+ // 不动 maxRecords/timeBudgetMs/callTimeoutMs——那三个仍属部署护栏,要改得改 YAML。
144
+ const bf = s?.backfillConcurrency ?? 0;
145
+ const backfillOverride = bf > 0 ? { backfill: { ...(cfg.backfill ?? {}), concurrency: bf } } : null;
146
+ if (!override && !budgets && !maxInput && !fallbacksTakeover && !chainEffort && !chainFallbacks && !effortInject && !layerChains && !hasChannel && !embedOverride && !cfOverride && !memExportOverride && !backfillOverride && !kwOverride && !rdOverride && !mdsOverride && !mpOverride)
129
147
  return cfg;
130
148
  return {
131
149
  ...cfg,
132
150
  ...(embedOverride ?? {}),
133
151
  ...(cfOverride ?? {}),
152
+ ...(kwOverride ?? {}),
153
+ ...(rdOverride ?? {}),
154
+ ...(mdsOverride ?? {}),
155
+ ...(mpOverride ?? {}),
134
156
  ...(memExportOverride ?? {}),
157
+ ...(backfillOverride ?? {}),
135
158
  llm: {
136
159
  ...cfg.llm,
137
160
  ...(effortInject ?? {}),
@@ -92,9 +92,10 @@ export declare const stateSchema: SchemaLike<RecallProjectionState>;
92
92
  /** view 校验(出网前调用;未知键容忍,同 slots 投影的语义边界)。 */
93
93
  export declare const viewSchema: SchemaLike<RecallView>;
94
94
  /** 投影需要的 L1 只读面(结构化声明,避免跨模块类型耦合)。
95
- * 判退场要 `validTo`/`metadata`,故直接 Pick 自 MemoryRecord,不在投影里自写时间比较。 */
95
+ * 判退场要 `validTo`/`metadata`,故直接 Pick 自 MemoryRecord,不在投影里自写时间比较。
96
+ * `name` 是名称蒸馏产出的短标题,优先作为展示标题。 */
96
97
  export interface RecallL1Face {
97
- getByIds(ids: string[]): Array<Pick<MemoryRecord, 'id' | 'type' | 'content' | 'priority' | 'metadata' | 'validTo'>>;
98
+ getByIds(ids: string[]): Array<Pick<MemoryRecord, 'id' | 'type' | 'content' | 'priority' | 'metadata' | 'validTo' | 'name'>>;
98
99
  }
99
100
  /** 投影需要的反馈只读面(缺席 = view 不带 rating,旧行为)。 */
100
101
  export interface RecallFeedbackFace {
@@ -77,11 +77,32 @@ export const viewSchema = {
77
77
  return value;
78
78
  },
79
79
  };
80
- /** 正文首行 → 标题(投影内不引 client 的 format 工具,保持服务端自足)。 */
80
+ /**
81
+ * 正文首行 → 标题(投影内不引 client 的 format 工具,保持服务端自足)。
82
+ * 按 markdown 语义解析:
83
+ * - H1(`# xxx`)是文档标题,直接作为记忆名称;
84
+ * - H2+(`## xxx`)是正文分节标记,跳过它取第一个实质文本行。
85
+ * 全部行都是 heading 时回落到第一个非空行。
86
+ */
81
87
  function firstLine(content) {
82
- const line = content.split('\n').find((l) => l.trim().length > 0) ?? content;
83
- const trimmed = line.trim();
84
- return trimmed.length > TITLE_MAX ? trimmed.slice(0, TITLE_MAX) : trimmed;
88
+ const lines = content.split('\n');
89
+ // 优先取 H1(文档级标题)
90
+ const h1 = lines.find((l) => /^#\s/.test(l.trim()));
91
+ if (h1)
92
+ return clampTitle(h1.trim());
93
+ // 无 H1 时跳过 H2+ 分节 heading,取第一个实质文本行
94
+ const body = lines.find((l) => {
95
+ const t = l.trim();
96
+ return t.length > 0 && !/^#{2,6}\s/.test(t);
97
+ });
98
+ if (body)
99
+ return clampTitle(body.trim());
100
+ // 全是 heading 的极端情况:回落第一个非空行
101
+ const fallback = lines.find((l) => l.trim().length > 0) ?? content;
102
+ return clampTitle(fallback.trim());
103
+ }
104
+ function clampTitle(s) {
105
+ return s.length > TITLE_MAX ? s.slice(0, TITLE_MAX) : s;
85
106
  }
86
107
  /** 正文全文 → 泡泡用(去首尾空白,超长截断补省略号)。 */
87
108
  function clampBody(content) {
@@ -132,7 +153,7 @@ function makeView(state, l1, feedback) {
132
153
  items.push({
133
154
  id: r.id,
134
155
  type: r.type,
135
- title: firstLine(r.content),
156
+ title: r.name || firstLine(r.content),
136
157
  priority: r.priority,
137
158
  body: clampBody(r.content),
138
159
  ...(scenario !== undefined && scenario.length > 0 ? { scenario } : {}),
@@ -36,6 +36,15 @@ export interface SlotsViewSlot {
36
36
  export interface SlotRefView {
37
37
  ref: string;
38
38
  title: string;
39
+ /**
40
+ * 记忆文件相对路径(`<type>/<name>.memory.md`)。
41
+ * 拼接绝对地址:
42
+ * - 有 workspaceId → `<workspaceId>/.agents/memory/<filePath>`
43
+ * - 无 workspaceId → `<memoriesRoot>/<filePath>`(由消费方自行解析根目录)
44
+ */
45
+ filePath?: string;
46
+ /** 记忆归属的工作区绝对路径;空串/undefined = global(无 workspace)。 */
47
+ workspaceId?: string;
39
48
  }
40
49
  export interface SlotsProjectionState {
41
50
  rev: number;
@@ -76,6 +85,8 @@ export interface L1LookupFace {
76
85
  id: string;
77
86
  content: string;
78
87
  type: string;
88
+ name?: string;
89
+ workspaceId?: string;
79
90
  }>;
80
91
  }
81
92
  export declare function registerSlotsProjection(ctx: Context, store: SlotStore, l1?: L1LookupFace): void;
@@ -1,4 +1,5 @@
1
1
  import { SLOT_KINDS, SLOT_STATUSES } from '../store/slots.js';
2
+ import { relPathFor } from '../store/memory-md.js';
2
3
  export const MEMORY_SLOTS_KEY = 'memorySlots';
3
4
  function invalid(what, detail) {
4
5
  throw new Error(`${what}: ${detail}`);
@@ -100,9 +101,18 @@ function resolveRefViewsUnsafe(recordRefs, l1) {
100
101
  const record = byId.get(ref);
101
102
  if (record === undefined)
102
103
  continue;
103
- const firstLine = record.content.split('\n', 1)[0] ?? '';
104
- const summary = firstLine.length > REF_TITLE_MAX ? firstLine.slice(0, REF_TITLE_MAX) + '…' : firstLine;
105
- views.push({ ref, title: '[' + record.type + '] ' + summary });
104
+ // 优先用 name 字段(名称蒸馏产出的短标题),无 name 时回落 content 首行截断。
105
+ const raw = record.name || (record.content.split('\n', 1)[0] ?? '');
106
+ const summary = raw.length > REF_TITLE_MAX ? raw.slice(0, REF_TITLE_MAX) + '…' : raw;
107
+ // 投射相对路径走单一事实源 `relPathFor`,与写盘/records-meta 端点同算法。
108
+ const filePath = relPathFor(record);
109
+ const ws = (record.workspaceId ?? '').trim();
110
+ views.push({
111
+ ref,
112
+ title: '[' + record.type + '] ' + summary,
113
+ filePath,
114
+ ...(ws.length > 0 ? { workspaceId: ws } : {}),
115
+ });
106
116
  }
107
117
  return views.length > 0 ? views : undefined;
108
118
  }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 存量记忆重蒸馏整形回填(redistill)。
3
+ *
4
+ * 职责:对**活跃**且 `scoreSkeleton` 判「骨架不齐备」的 L1 记录,**一条一个请求**送 LLM
5
+ * 把正文重排成 G2 分类型的 `## 节名` 分节骨架,并顺带产 `dsh_keywords` / `dsh_usage_scenario`;
6
+ * 只有**整形后骨架分严格更优且无空心标题**才写回(并盖当前整形版本戳),否则留原文**不打标记**——
7
+ * 下轮仍可再试(用户定:「成功替换的」才不再进入重蒸馏)。
8
+ *
9
+ * 与 keyword-backfill 的关键差异(为什么它更重一档):
10
+ * - **改 content** ⇒ 写回必须走 `L1Store.appendNew`(追加 JSONL 事实源 + upsertL1 重嵌+FTS +
11
+ * fireFileSync 重导出),**不能**走 `MemoryBackend.patchMetadata*`(只改元数据、不动正文),
12
+ * 也**不能**走 worker backend(`memory-backend.ts` 明示 upsert 会重嵌、worker 拿不到
13
+ * embedding service 会写零向量毁数据)。故本模块 deps 直取 `l1: L1Store`(进程内)。
14
+ * - **入口 `vectorsReady()` 守卫**:`upsertL1InTx` 先删向量行、仅非零 embedding 才插回;
15
+ * embed 未就绪时改正文会静默丢该记录向量。故向量未就绪 ⇒ 整轮回跳过(整形是可选优化,不抢这个险)。
16
+ *
17
+ * 安全边界(继承 keyword/wing 回填):
18
+ * - 单飞:同时最多一个任务,重复触发返回 running(不排队不叠加)
19
+ * - 后台执行:端点立即返回,逐条推进并**每条即时提交**,不阻塞 RPC/管线
20
+ * - 失败可退:LLM 失败/输出非法/质量闸不过 ⇒ 原记录零改动(写回只发生在严格更优且 embed 就绪时)
21
+ * - 有界:单次最多 cfg.backfill.maxRecords 条 + timeBudgetMs 墙钟;其余计入 deferred 可再触发
22
+ * - 排除靠**版本戳**:成功写回盖 `dsh_reshape_ver = RESHAPE_SCHEMA_VERSION`;扫描只排「当前版本
23
+ * 已整过」的(存量兼容:只有旧布尔 `dsh_reshape_tried` 且正文确为 `## 分节`式也算已整)。
24
+ * 整形规则/提示词改变时把 `RESHAPE_SCHEMA_VERSION` 递增,旧产物即自动重新入列。
25
+ * - 门控关(`cfg.redistill.enabled = false`):入口立即跳过——不动 content、FTS、投射逐字不变
26
+ *
27
+ * 形状约定(用户定):**按单元、串行、不合并**——一个请求只装一条记忆,跑完一条再发下一条。
28
+ * 扫描期就用 SQL 退场判据 + 骨架判据筛出真目标(旧实现收 1221 条候选、每批 20 条只命中 ~4 条)。
29
+ * `cfg.backfill.concurrency` 默认 1(串行);拖高概览页滑块可临时多路在飞(上限 8)。
30
+ * 注意:本任务直接调 `callLLM`、不进 runner 队列,拿不到“live 优先”的让位保护。
31
+ */
32
+ import type { Context } from '@deepseek-ai/cordis';
33
+ import type { L1Store } from './store/l1.js';
34
+ import type { MemoryConfig } from './config.js';
35
+ import type { MemoryLogger, MemoryRecord } from './types.js';
36
+ /**
37
+ * 整形**版本戳**:只在**成功替换**时写入,值为当前整形规则版本(`RESHAPE_SCHEMA_VERSION`)。
38
+ * 扫描阶段据此排除「已按当前规则整过形」的记录——见 `isReshapedAtCurrentVersion`。
39
+ */
40
+ export declare const RESHAPE_VER_KEY = "dsh_reshape_ver";
41
+ /**
42
+ * 当前整形规则版本。**改动整形骨架/提示词/输出契约时必须 +1**,否则旧产物不会重跑。
43
+ * v1 = 行内标签式(`规则:/触发时机:`,已废);v2 = `## 节名` 分节 + 空心标题闸。
44
+ */
45
+ export declare const RESHAPE_SCHEMA_VERSION = 2;
46
+ export interface RedistillBackfillDeps {
47
+ ctx: Context;
48
+ cfg: MemoryConfig;
49
+ /** 直取 L1Store(进程内):改 content 要 appendNew 重嵌 + 追加 JSONL + 投射点火,不经 worker backend。 */
50
+ l1: L1Store;
51
+ logger: MemoryLogger;
52
+ /**
53
+ * 运行时**生效并发**读取器(可选):给正在跑的任务即时改并发用——池子每完成一条重读一次。
54
+ * 缺省 = 启动时读一次 `cfg.backfill.concurrency`(旧行为)。
55
+ */
56
+ concurrency?: () => number | undefined;
57
+ }
58
+ export interface RedistillBackfillState {
59
+ running: boolean;
60
+ /** 整形成功写回条数。 */
61
+ updated: number;
62
+ /** 质量闸判不写(留原文)条数。 */
63
+ skipped: number;
64
+ /** 失败/跳过(LLM 失败、id 配不上、embed 掉线)条数。 */
65
+ failed: number;
66
+ /** 分批进度(UI 轮询据此显示「扫描中 / 第几条」,否则运行期只有一个「整形中…」): */
67
+ /** 每个请求处理的条数——本任务已改成逐条,永远为 1(保留字段与 keyword 同形状)。 */
68
+ chunk: number;
69
+ /** 扫描阶段确定的**真目标**总数(骨架不齐备且未试过;扫描未完成时为已发现数)。 */
70
+ candidates: number;
71
+ /** 本次已完成(送过 LLM 且已提交结果)条数。**在请求完成时才自增**——
72
+ * 在开始处加会让“全部超时失败”跟“全部成功”长得一模一样。 */
73
+ processed: number;
74
+ /** 仍在扫描阶段(此阶段 candidates 还在动,UI 应报「扫描中」而非「第 N 条」)。 */
75
+ scanning: boolean;
76
+ /** 已完成条数(与 processed 同值,保留为与 keyword 共用的进度形状)。 */
77
+ batchesDone: number;
78
+ /** 本次要处理的总条数。 */
79
+ batchTotal: number;
80
+ /** LLM 批调用并行度。 */
81
+ concurrency: number;
82
+ /** 预算/上限用尽未处理、留下次的条数。 */
83
+ deferred: number;
84
+ }
85
+ export declare function redistillBackfillState(): RedistillBackfillState;
86
+ /**
87
+ * 空心标题检测:某个 `## 节名` 到下一标题之间没有实质文字。
88
+ *
89
+ * 为什么需要它:判定器(`scoreSkeleton`)按子串命中认骨架,而标题词本身就是那个子串——
90
+ * 只写 `## 触发时机` 不写内容就能白得一个命中。不堵这个闸,“加分才写回”会退化成
91
+ * “加空标题就写回”,正文反而被掏空了。
92
+ */
93
+ export declare function hasHollowHeading(content: string): boolean;
94
+ /** 质量闸:整形后骨架命中数严格上升(或旧不齐备且新齐备)且正文非空、无空心标题 → 判可写。 */
95
+ export declare function reshapePassesGate(type: string, oldContent: string, newContent: string): boolean;
96
+ /**
97
+ * 是否已按**当前版本**整理过 → 扫描阶段排除(用户定:「成功替换的」才不再进入)。
98
+ *
99
+ * 判据:
100
+ * - 带当前版本戳(`dsh_reshape_ver === RESHAPE_SCHEMA_VERSION`)⇒ 已成功替换,排除;
101
+ * - 旧存量兼容:只有布尔 `dsh_reshape_tried`(成功与留原文都打过,无法区分)+ 正文确实是
102
+ * `## 分节`式 ⇒ 视为已整形排除;**不带标题的则重新入列**(它们是「试过但没改成」的,
103
+ * 或首轮行内标签式产物,都该重跑)。
104
+ */
105
+ export declare function isReshapedAtCurrentVersion(meta: Record<string, unknown> | undefined, content: string): boolean;
106
+ /** 启动后台重蒸馏回填;已在运行返回 false(单飞)。 */
107
+ export declare function startRedistillBackfill(deps: RedistillBackfillDeps): {
108
+ started: boolean;
109
+ running: boolean;
110
+ };
111
+ /**
112
+ * 回填主体(导出供单测 await 到完成;生产走 `startRedistillBackfill` 的单飞后台包装)。
113
+ * 门控关或向量未就绪 ⇒ 入口即返回,不碰任何记录、FTS、投射。
114
+ *
115
+ * 阶段划分对齐反刍重标定(`pipeline/relabel.ts`):**机械扫描期把筛子下推到底**,
116
+ * LLM 期只吃真目标、逐条推进、**每条即时提交**、条间受墙钟预算约束。
117
+ * 形状约定(用户定):**按单元、串行、不合并**——`concurrency` 默认 1(跑完一条再发下一条),
118
+ * 拖高概览页滑块只是临时提速。旧形状(20 条一批)真机把单请求拖到 200s+ 并在超时线上被掐。
119
+ */
120
+ export declare function runRedistillBackfillOnce(deps: RedistillBackfillDeps): Promise<void>;
121
+ /** 单条整形结果。 */
122
+ export interface ReshapeOneResult {
123
+ content: string;
124
+ keywords: string[];
125
+ usageScenario: string[];
126
+ }
127
+ /**
128
+ * 整形**一条**记忆(一个请求只装一条)。失败/输出非法返回 null(原记录不动)。
129
+ *
130
+ * 导出不带 ctx/cfg 以外的参数,供单测注入桩 LLM。旧名 `reshapeChunk`(多条一次)已废:
131
+ * 合并请求把爆炸半径从 1 条放大到 20 条,而实测逐条只花 2.6s,合并毫无收益。
132
+ */
133
+ export declare function reshapeOne(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, rec: MemoryRecord, callTimeoutMs?: number): Promise<ReshapeOneResult | null>;