dsh-prime-memory 0.25.0 → 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/dist/stats.js CHANGED
@@ -32,8 +32,9 @@ import { WING_CATALOG, WING_FALLBACK } from './types.js';
32
32
  import { isRoomSlug } from './metadata-validators.js';
33
33
  import { InProcMemoryBackend } from './store/memory-backend.js';
34
34
  import { isWingCorner } from './store/session-modes.js';
35
- import { startWingBackfill } from './wing-backfill.js';
35
+ import { startWingBackfill, wingBackfillState } from './wing-backfill.js';
36
36
  import { startKeywordBackfill, keywordBackfillState } from './keyword-backfill.js';
37
+ import { startRedistillBackfill, redistillBackfillState } from './redistill-backfill.js';
37
38
  import { errDetail } from './util/filelog.js';
38
39
  import { snapshotTokenCost } from './token-cost.js';
39
40
  import { computeMaturity } from './util/maturity.js';
@@ -42,13 +43,31 @@ import { relinkWorkspaces } from './pipeline/workspace-relink.js';
42
43
  import { reconcileLedger } from './store/memory-md-export.js';
43
44
  import { mdRunStatus, runProjection } from './store/memory-md-run.js';
44
45
  import { disableProjectionFile } from './store/memory-md-disable.js';
45
- import { MEMORY_TYPE_DIRS, UNSORTED_DIR, resolveMemoriesRoot, targetFor } from './store/memory-md.js';
46
+ import { MEMORY_TYPE_DIRS, UNSORTED_DIR, resolveMemoriesRoot, targetFor, relPathFor } from './store/memory-md.js';
46
47
  import { ensureTypeSkeleton } from './store/memory-md-skeleton.js';
47
48
  import { sweepOrphanFiles } from './store/memory-md-orphan.js';
48
49
  import { collectProjectionFiles } from './store/memory-md-archive.js';
49
50
  import { syncFromFiles } from './pipeline/md-sync.js';
50
51
  const require = createRequire(import.meta.url);
51
52
  export const PLUGIN_VERSION = require('../package.json').version;
53
+ /**
54
+ * 批扫模块的内部状态 → 契约进度体(keyword 与 redistill 同形状,故共用一个映射)。
55
+ *
56
+ * 写成显式字段而不是 `as`:两边 state 类型各自演化时,这里漏一个字段就是编译错,
57
+ * 而强转会造出一个能编译、但面板上永远显示旧值的进度。
58
+ */
59
+ function backfillProgress(s) {
60
+ return {
61
+ chunk: s.chunk,
62
+ candidates: s.candidates,
63
+ processed: s.processed,
64
+ scanning: s.scanning,
65
+ batchesDone: s.batchesDone,
66
+ batchTotal: s.batchTotal,
67
+ concurrency: s.concurrency,
68
+ deferred: s.deferred,
69
+ };
70
+ }
52
71
  /**
53
72
  * 端点全集运行时清单(与 tests/contract-keys.test.ts 的 ENDPOINTS 及
54
73
  * contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
@@ -72,10 +91,12 @@ export const MEMORY_ENDPOINTS = [
72
91
  'dsh-memory/room-admin',
73
92
  'dsh-memory/wing-backfill',
74
93
  'dsh-memory/keyword-backfill',
94
+ 'dsh-memory/redistill-backfill',
75
95
  'dsh-memory/session-stats',
76
96
  'dsh-memory/settings-get',
77
97
  'dsh-memory/settings-set',
78
98
  'dsh-memory/list-records',
99
+ 'dsh-memory/records-meta',
79
100
  'dsh-memory/records-delete',
80
101
  'dsh-memory/receipts',
81
102
  'dsh-memory/conflicts',
@@ -633,10 +654,11 @@ export async function handleEndpoint(endpoint, payload, deps) {
633
654
  case 'dsh-memory/mem-sync': {
634
655
  // P2 反向同步:文件 → 库。与导出的**风险不对称**——导出只写文件,本项会写库,
635
656
  // 所以多两道门:要先开投射(没导出过 = 账本为空 = 判不出“谁变了”),要高权限。
636
- const syn = cfg.mdSync;
657
+ const ec = effectiveCfg(cfg, live);
658
+ const syn = ec.mdSync;
637
659
  if (syn?.enabled !== true)
638
- throw new Error('md 反向同步未开启(cfg.mdSync.enabled)');
639
- if (cfg.memExport?.enabled !== true)
660
+ throw new Error('md 反向同步未开启(概览页开关,或 config mdSync.enabled)');
661
+ if (ec.memExport?.enabled !== true)
640
662
  throw new Error('md 反向同步需先开启 md 投射(cfg.memExport.enabled)');
641
663
  if (!live?.get().memoryMutate)
642
664
  throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能回灌文件侧修改。');
@@ -794,7 +816,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
794
816
  }
795
817
  // ── 轨3 外部记忆来源(Connect):登记/列表/退役/只读扫描。整面默认关(cfg.memport.enabled)。
796
818
  case 'dsh-memory/memory-sources-list': {
797
- const enabled = cfg.memport?.enabled === true;
819
+ const enabled = effectiveCfg(cfg, live).memport?.enabled === true;
798
820
  const v = {
799
821
  sources: enabled && memorySources
800
822
  ? memorySources.list().map((e) => ({
@@ -813,8 +835,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
813
835
  return v;
814
836
  }
815
837
  case 'dsh-memory/memory-sources-add': {
816
- if (cfg.memport?.enabled !== true)
817
- throw new Error('外部记忆来源未启用(config memport.enabled)。');
838
+ if (effectiveCfg(cfg, live).memport?.enabled !== true)
839
+ throw new Error('外部记忆来源未启用(概览页开关,或 config memport.enabled)。');
818
840
  if (!memorySources)
819
841
  throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
820
842
  if (!live?.get().memoryMutate)
@@ -834,8 +856,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
834
856
  return v;
835
857
  }
836
858
  case 'dsh-memory/memory-sources-retire': {
837
- if (cfg.memport?.enabled !== true)
838
- throw new Error('外部记忆来源未启用(config memport.enabled)。');
859
+ if (effectiveCfg(cfg, live).memport?.enabled !== true)
860
+ throw new Error('外部记忆来源未启用(概览页开关,或 config memport.enabled)。');
839
861
  if (!memorySources)
840
862
  throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
841
863
  if (!live?.get().memoryMutate)
@@ -853,8 +875,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
853
875
  return v;
854
876
  }
855
877
  case 'dsh-memory/memport-scan': {
856
- if (cfg.memport?.enabled !== true) {
857
- const v = { enabled: false, sources: [], notice: '外部记忆扫描未启用(config memport.enabled)。' };
878
+ if (effectiveCfg(cfg, live).memport?.enabled !== true) {
879
+ const v = { enabled: false, sources: [], notice: '外部记忆扫描未启用(概览页开关,或 config memport.enabled)。' };
858
880
  return v;
859
881
  }
860
882
  if (!memorySources) {
@@ -864,7 +886,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
864
886
  const p = (payload ?? {});
865
887
  const all = memorySources.listEnabled();
866
888
  const targets = p.sourceId ? all.filter((e) => e.id === p.sourceId) : all;
867
- const maxRecords = cfg.memport?.scanMaxRecords ?? 5000;
889
+ const maxRecords = effectiveCfg(cfg, live).memport?.scanMaxRecords ?? 5000;
868
890
  // 只读扫描:不写库(写走 memory_import)。逐个标扫描时间(失败不影响预览)。
869
891
  const sources = await scanSources({ sources: targets, l1: stores.l1, maxRecords });
870
892
  const nowIso = new Date().toISOString();
@@ -1134,6 +1156,10 @@ export async function handleEndpoint(endpoint, payload, deps) {
1134
1156
  return v;
1135
1157
  }
1136
1158
  case 'dsh-memory/wing-backfill': {
1159
+ // probe=true 只读快照:不启动(UI 离开区域再回来时恢复在途任务用)。
1160
+ if (payload?.probe === true) {
1161
+ return { started: false, running: wingBackfillState().running };
1162
+ }
1137
1163
  const r = startWingBackfill({
1138
1164
  ctx: deps.ctx,
1139
1165
  cfg: deps.cfg,
@@ -1143,17 +1169,47 @@ export async function handleEndpoint(endpoint, payload, deps) {
1143
1169
  return r;
1144
1170
  }
1145
1171
  case 'dsh-memory/keyword-backfill': {
1146
- // 门控关:不启动,如实报 enabled=false(面板据此提示“先开关键词回填开关”)。
1147
- if (!cfg.keyword?.enabled)
1148
- return { started: false, running: false, enabled: false, updated: 0, failed: 0 };
1172
+ // 门控看生效配置(live.keywordEnabled 覆盖部署 cfg.keyword.enabled);关时不启动。
1173
+ const ec = effectiveCfg(cfg, live);
1174
+ const st0 = keywordBackfillState();
1175
+ // probe=true 只读快照:不启动(UI 离开区域再回来时恢复在途任务用)。
1176
+ if (payload?.probe === true) {
1177
+ return { started: false, running: st0.running, enabled: ec.keyword?.enabled === true, updated: st0.updated, failed: st0.failed, progress: backfillProgress(st0) };
1178
+ }
1179
+ if (!ec.keyword?.enabled)
1180
+ return { started: false, running: false, enabled: false, updated: 0, failed: 0, progress: backfillProgress(st0) };
1149
1181
  const r = startKeywordBackfill({
1150
1182
  ctx: deps.ctx,
1151
- cfg: deps.cfg,
1183
+ cfg: ec,
1152
1184
  backend: stores.backend ?? new InProcMemoryBackend(stores.l1),
1153
1185
  logger: deps.logger,
1186
+ // 运行时并发读取器:拖概览页滑块对**正在跑**的任务即时生效(弹性池每完成一条重读)。
1187
+ concurrency: () => effectiveCfg(cfg, live).backfill?.concurrency,
1154
1188
  });
1155
1189
  const st = keywordBackfillState();
1156
- return { started: r.started, running: r.running, enabled: true, updated: st.updated, failed: st.failed };
1190
+ return { started: r.started, running: r.running, enabled: true, updated: st.updated, failed: st.failed, progress: backfillProgress(st) };
1191
+ }
1192
+ case 'dsh-memory/redistill-backfill': {
1193
+ // 门控看生效配置(live.redistillEnabled 覆盖部署 cfg.redistill.enabled);关时不启动。
1194
+ const ec = effectiveCfg(cfg, live);
1195
+ const st0 = redistillBackfillState();
1196
+ // probe=true 只读快照:不启动(UI 离开区域再回来时恢复在途任务用)。
1197
+ if (payload?.probe === true) {
1198
+ return { started: false, running: st0.running, enabled: ec.redistill?.enabled === true, updated: st0.updated, skipped: st0.skipped, failed: st0.failed, progress: backfillProgress(st0) };
1199
+ }
1200
+ if (!ec.redistill?.enabled)
1201
+ return { started: false, running: false, enabled: false, updated: 0, skipped: 0, failed: 0, progress: backfillProgress(st0) };
1202
+ // 改 content 需进程内重嵌 + 追加 JSONL + 投射点火,直取 stores.l1(不经 worker backend)。
1203
+ const r = startRedistillBackfill({
1204
+ ctx: deps.ctx,
1205
+ cfg: ec,
1206
+ l1: stores.l1,
1207
+ logger: deps.logger,
1208
+ // 运行时并发读取器:拖概览页滑块对**正在跑**的任务即时生效(弹性池每完成一条重读)。
1209
+ concurrency: () => effectiveCfg(cfg, live).backfill?.concurrency,
1210
+ });
1211
+ const st = redistillBackfillState();
1212
+ return { started: r.started, running: r.running, enabled: true, updated: st.updated, skipped: st.skipped, failed: st.failed, progress: backfillProgress(st) };
1157
1213
  }
1158
1214
  // ── 会话级统计(悬浮卡信息区;热路径端点,见 SessionInfoSource 的零 I/O 硬规则) ──
1159
1215
  case 'dsh-memory/session-stats': {
@@ -1264,7 +1320,12 @@ export async function handleEndpoint(endpoint, payload, deps) {
1264
1320
  memoryMutate: false,
1265
1321
  conflictFreeze: live?.get()?.conflictFreeze === true,
1266
1322
  skillsEnabled: live?.get()?.skillsEnabled === true,
1323
+ keywordEnabled: live?.get()?.keywordEnabled === true,
1324
+ redistillEnabled: live?.get()?.redistillEnabled === true,
1325
+ mdSyncEnabled: live?.get()?.mdSyncEnabled === true,
1326
+ memportEnabled: live?.get()?.memportEnabled === true,
1267
1327
  mdExportMaxRecords: live?.get()?.mdExportMaxRecords ?? 0,
1328
+ backfillConcurrency: live?.get()?.backfillConcurrency ?? 0,
1268
1329
  }),
1269
1330
  // 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
1270
1331
  ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
@@ -1303,7 +1364,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
1303
1364
  const patch = (payload ?? {});
1304
1365
  const clean = {};
1305
1366
  // 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
1306
- for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'skillsEnabled', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate']) {
1367
+ for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'skillsEnabled', 'keywordEnabled', 'redistillEnabled', 'mdSyncEnabled', 'memportEnabled', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate']) {
1307
1368
  if (typeof patch[key] === 'boolean')
1308
1369
  clean[key] = patch[key];
1309
1370
  }
@@ -1312,6 +1373,11 @@ export async function handleEndpoint(endpoint, payload, deps) {
1312
1373
  if (typeof patch.mdExportMaxRecords === 'number' && Number.isFinite(patch.mdExportMaxRecords)) {
1313
1374
  clean.mdExportMaxRecords = Math.min(10_000, Math.max(0, Math.floor(patch.mdExportMaxRecords)));
1314
1375
  }
1376
+ // 回填并行度运行时覆盖(概览页滑块):不收白名单 = “写成功但被静默丢”,
1377
+ // 而用户会拿着“并行 1”的 UI 去判断串行跑的结果——比没有这个旋钮更坏。
1378
+ if (typeof patch.backfillConcurrency === 'number' && Number.isFinite(patch.backfillConcurrency)) {
1379
+ clean.backfillConcurrency = Math.min(8, Math.max(0, Math.floor(patch.backfillConcurrency)));
1380
+ }
1315
1381
  // 运行时统一路由链:结构校验后整体写入(空数组 = 回到跟随部署配置)
1316
1382
  if (patch.distillChain !== undefined) {
1317
1383
  const err = validateDistillChain(patch.distillChain);
@@ -1630,6 +1696,37 @@ export async function handleEndpoint(endpoint, payload, deps) {
1630
1696
  })),
1631
1697
  };
1632
1698
  }
1699
+ case 'dsh-memory/records-meta': {
1700
+ const p = (payload ?? {});
1701
+ if (!Array.isArray(p.ids) || p.ids.length === 0)
1702
+ throw new Error('ids 必须是非空字符串数组');
1703
+ const ids = p.ids.filter((x) => typeof x === 'string' && x.length > 0).slice(0, 50);
1704
+ if (ids.length === 0)
1705
+ throw new Error('ids 过滤后为空');
1706
+ const records = stores.l1.getByIds(ids);
1707
+ const items = records.map((r) => {
1708
+ // 与写盘、槽位投影同一算法,不各自抄一份清洗规则。
1709
+ const filePath = relPathFor(r);
1710
+ return {
1711
+ id: r.id,
1712
+ name: r.name ?? null,
1713
+ type: r.type,
1714
+ priority: r.priority,
1715
+ content: r.content,
1716
+ maturity: computeMaturity({ priority: r.priority, usage: null, agingWeight: null, mark: null }),
1717
+ createdAt: r.createdAt ? new Date(r.createdAt).toISOString() : null,
1718
+ updatedAt: r.updatedAt ? new Date(r.updatedAt).toISOString() : null,
1719
+ scene: r.scene_name,
1720
+ scope: (r.scope === 'workspace' ? 'workspace' : 'global'),
1721
+ workspaceId: r.scope === 'workspace' ? (r.workspaceId ?? '') || null : null,
1722
+ ...(Array.isArray(r.metadata?.dsh_keywords) ? { keywords: r.metadata.dsh_keywords.filter((k) => typeof k === 'string' && k !== '') } : {}),
1723
+ ...(Array.isArray(r.metadata?.dsh_usage_scenario) ? { usageScenario: r.metadata.dsh_usage_scenario } : {}),
1724
+ filePath,
1725
+ retired: r.validTo !== undefined,
1726
+ };
1727
+ });
1728
+ return { items };
1729
+ }
1633
1730
  case 'dsh-memory/records-delete': {
1634
1731
  // 面板高权限删除指定记忆;写入删权限门(memoryMutate)防御。
1635
1732
  //
@@ -122,6 +122,18 @@ export declare class L1Store {
122
122
  * 配套写回必须用 `patchMetadata()` —— 本方法拿不到 content,用 upsert 会把正文清空。
123
123
  */
124
124
  allLite(limit: number, offset: number): L1MetaLite[];
125
+ /**
126
+ * 游标分批取**活跃**记录的骨架判定输入(含 content)——redistill 扫描阶段专用。
127
+ *
128
+ * 与 `allLite` 的分工:本方法**只为读**(判骨架),写回仍走 `appendNew`(改正文)/
129
+ * `patchMetadata`(只打标)——不得拿本投影去 upsert,退场行已被 SQL 排除。
130
+ */
131
+ activeForSkeleton(limit: number, offset: number): Array<{
132
+ id: string;
133
+ type: string;
134
+ content: string;
135
+ metadata: Record<string, unknown>;
136
+ }>;
125
137
  /**
126
138
  * 只更新 metadata_json 的最小写回(不动 content 与其它列)。
127
139
  * 返回 false 表示 id 不存在或写入失败——调用方须记账,不许静默。
package/dist/store/l1.js CHANGED
@@ -149,6 +149,15 @@ export class L1Store {
149
149
  allLite(limit, offset) {
150
150
  return this.db.getAllL1Lite(limit, offset);
151
151
  }
152
+ /**
153
+ * 游标分批取**活跃**记录的骨架判定输入(含 content)——redistill 扫描阶段专用。
154
+ *
155
+ * 与 `allLite` 的分工:本方法**只为读**(判骨架),写回仍走 `appendNew`(改正文)/
156
+ * `patchMetadata`(只打标)——不得拿本投影去 upsert,退场行已被 SQL 排除。
157
+ */
158
+ activeForSkeleton(limit, offset) {
159
+ return this.db.getActiveL1ForSkeleton(limit, offset);
160
+ }
152
161
  /**
153
162
  * 只更新 metadata_json 的最小写回(不动 content 与其它列)。
154
163
  * 返回 false 表示 id 不存在或写入失败——调用方须记账,不许静默。
@@ -105,6 +105,14 @@ export declare function recordDigest(r: Pick<MemoryRecord, 'content'>): string;
105
105
  * 免得后来人以为"回落名也能双向"。
106
106
  */
107
107
  export declare function fileStemFor(r: Pick<MemoryRecord, 'id' | 'name' | 'content'>): string;
108
+ /**
109
+ * 一条记录的投射相对路径 `<dir>/<stem>.memory.md`(分隔符恒 `/`)。
110
+ *
111
+ * 单一事实源:`renderRecord` 写盘、`dsh-memory/records-meta` 端点、槽位投影的
112
+ * `refViews.filePath` 都走这里。各自抄一份清洗规则必然漂移——漂移之后「投影给的地址」
113
+ * 和「磁盘上的真文件」对不上,而这三处本来就是给人核对同一个事实用的。
114
+ */
115
+ export declare function relPathFor(r: Pick<MemoryRecord, 'id' | 'name' | 'content' | 'type'>): string;
108
116
  /**
109
117
  * 渲染一条记录。
110
118
  *
@@ -184,6 +184,17 @@ export function fileStemFor(r) {
184
184
  // 借 sanitizeFilename 的字符/保留名纪律,再剥掉它补的 .md(我们后面统一补)。
185
185
  return sanitizeFilename(base).replace(/\.md$/i, '') || `mem-${r.id.slice(-8)}`;
186
186
  }
187
+ /**
188
+ * 一条记录的投射相对路径 `<dir>/<stem>.memory.md`(分隔符恒 `/`)。
189
+ *
190
+ * 单一事实源:`renderRecord` 写盘、`dsh-memory/records-meta` 端点、槽位投影的
191
+ * `refViews.filePath` 都走这里。各自抄一份清洗规则必然漂移——漂移之后「投影给的地址」
192
+ * 和「磁盘上的真文件」对不上,而这三处本来就是给人核对同一个事实用的。
193
+ */
194
+ export function relPathFor(r) {
195
+ const dir = MEMORY_TYPE_DIRS.has(r.type) ? r.type : UNSORTED_DIR;
196
+ return `${dir}/${fileStemFor(r)}.memory.md`;
197
+ }
187
198
  /** 把值写成可解析回读的 frontmatter 标量/数组(JSON 编码,转义由它负责)。 */
188
199
  function fmLine(key, value) {
189
200
  return `${key}: ${JSON.stringify(value)}`;
@@ -195,9 +206,9 @@ function fmLine(key, value) {
195
206
  * @returns `relPath`(相对投射根,已归一分隔符)与 `text`(全文)。
196
207
  */
197
208
  export function renderRecord(r) {
198
- const dir = MEMORY_TYPE_DIRS.has(r.type) ? r.type : UNSORTED_DIR;
209
+ const relPath = normalize(relPathFor(r)).replace(/\\/g, '/');
210
+ // frontmatter 的 `name` 必须与文件名干同一值,所以单独再取一次(同一纯函数,不会漂)。
199
211
  const stem = fileStemFor(r);
200
- const relPath = normalize(`${dir}/${stem}.memory.md`).replace(/\\/g, '/');
201
212
  const scenario = Array.isArray(r.metadata?.dsh_usage_scenario)
202
213
  ? (r.metadata?.dsh_usage_scenario).filter((s) => typeof s === 'string')
203
214
  : [];
@@ -229,6 +229,23 @@ export declare class MemoryDb {
229
229
  * 巡检期间即使有写回也不会有分页漂移;updated_time 会被写回改动。
230
230
  */
231
231
  getAllL1Lite(limit: number, offset: number): L1MetaLite[];
232
+ /**
233
+ * 游标分批取**活跃** L1 的骨架判定所需列(`id / type / content / metadata`)。
234
+ *
235
+ * 为什么单开一个方法而不是复用 `getAllL1Lite`:后者刻意不返 content(防写回把正文清空),
236
+ * 而 redistill 的**扫描阶段就要用 content 判骨架**——把筛子下推到扫描期,才能像反刍
237
+ * 重标定那样“只把真目标送 LLM”,而不是每批 hydrate 20 条候选后发现只有 4 条要整形。
238
+ *
239
+ * 退场判据走 `retiredL1Sql()`(全库唯一事实源)下推到 SQL:活跃集本就只占全库
240
+ * 两成(真机 289/1221),先筛掉退场行可把扫描代价降到五分之一。
241
+ * 分页同 `getAllL1Lite`:`ORDER BY record_id`(主键不变 ⇒ 无分页漂移)。
242
+ */
243
+ getActiveL1ForSkeleton(limit: number, offset: number): Array<{
244
+ id: string;
245
+ type: string;
246
+ content: string;
247
+ metadata: Record<string, unknown>;
248
+ }>;
232
249
  /**
233
250
  * **只**更新 metadata_json(顺带 updated_time),绝不碰 content 或其它列。
234
251
  *
@@ -1275,6 +1275,39 @@ export class MemoryDb {
1275
1275
  return [];
1276
1276
  }
1277
1277
  }
1278
+ /**
1279
+ * 游标分批取**活跃** L1 的骨架判定所需列(`id / type / content / metadata`)。
1280
+ *
1281
+ * 为什么单开一个方法而不是复用 `getAllL1Lite`:后者刻意不返 content(防写回把正文清空),
1282
+ * 而 redistill 的**扫描阶段就要用 content 判骨架**——把筛子下推到扫描期,才能像反刍
1283
+ * 重标定那样“只把真目标送 LLM”,而不是每批 hydrate 20 条候选后发现只有 4 条要整形。
1284
+ *
1285
+ * 退场判据走 `retiredL1Sql()`(全库唯一事实源)下推到 SQL:活跃集本就只占全库
1286
+ * 两成(真机 289/1221),先筛掉退场行可把扫描代价降到五分之一。
1287
+ * 分页同 `getAllL1Lite`:`ORDER BY record_id`(主键不变 ⇒ 无分页漂移)。
1288
+ */
1289
+ getActiveL1ForSkeleton(limit, offset) {
1290
+ if (this.degraded)
1291
+ return [];
1292
+ if (limit <= 0)
1293
+ return [];
1294
+ try {
1295
+ const rows = this.db
1296
+ .prepare(`SELECT record_id, type, content, metadata_json FROM l1_records
1297
+ WHERE NOT (${retiredL1Sql()}) ORDER BY record_id LIMIT ? OFFSET ?`)
1298
+ .all(limit, offset);
1299
+ return rows.map((r) => ({
1300
+ id: r.record_id,
1301
+ type: r.type,
1302
+ content: r.content ?? '',
1303
+ metadata: parseMetadataJson(r.metadata_json),
1304
+ }));
1305
+ }
1306
+ catch (err) {
1307
+ this.logger?.warn(`${TAG} L1 骨架取数失败(返回空,本轮跳过): ${err instanceof Error ? err.message : String(err)}`);
1308
+ return [];
1309
+ }
1310
+ }
1278
1311
  /**
1279
1312
  * **只**更新 metadata_json(顺带 updated_time),绝不碰 content 或其它列。
1280
1313
  *
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * 存量未打标记录的一键回填(task_15):从 WingWheel"未打标 M"入口触发,
3
- * 对 metadata 无 wing 的 L1 记录按 8 角词表批量补打标签(复用 L1 抽取的打标路径:
3
+ * 对 metadata 无 wing 的 L1 记录按 8 角词表**逐条**补打标签(复用 L1 抽取的打标路径:
4
4
  * 同一候选词表与措辞,经既有蒸馏 LLM 路由)。
5
5
  *
6
6
  * 安全边界:
7
7
  * - **单飞**:同时最多一个回填任务,重复触发直接返回 running(不排队不叠加);
8
- * - **后台执行**:端点立即返回,逐块推进(每块 20 条),不阻塞 RPC/管线;
9
- * - **失败可退**:LLM 失败/输出不合法只跳过当前块,原记录零改动(标签只写成功的);
8
+ * - **后台执行**:端点立即返回,**逐条推进、跑完一条再发下一条**,不阻塞 RPC/管线;
9
+ * - **失败可退**:LLM 失败/输出不合法只跳过当前条,原记录零改动(标签只写成功的);
10
10
  * - **有界**:单次任务最多处理 MAX_RECORDS 条,防止一次跑飞;其余可再次触发。
11
+ *
12
+ * 形状约定(用户定):**按单元、串行、不合并**——一个请求只装一条记忆。旧形状把 20 条
13
+ * 合并进一个 prompt(一次截断/空输出就整批丢弃),逐条只把爆炸半径限制在 1 条。
11
14
  */
12
15
  import type { Context } from '@deepseek-ai/cordis';
13
16
  import { type MemoryLogger, type MemoryRecord } from './types.js';
@@ -32,16 +35,15 @@ export declare function startWingBackfill(deps: HallBackfillDeps): {
32
35
  started: boolean;
33
36
  running: boolean;
34
37
  };
35
- /** Wing 标注器(导出):一键回填与反刍重标定共用同一 LLM 路径与措辞。 */
36
- export declare function labelWingChunk(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, chunk: MemoryRecord[], candidates: string): Promise<Array<{
37
- record: MemoryRecord;
38
- wing: string;
39
- }>>;
40
38
  /**
41
- * 涌现标签标注器(导出):为一批记录提炼 slug 标签(tags)——Room 的前身。
42
- * 与 Wing 标注共用 LLM 路由(layer='l1-extract');失败返回空数组,调用方零改动。
39
+ * Wing 标注器(导出):一键回填与反刍重标定共用同一 LLM 路径与措辞。
40
+ *
41
+ * **一个请求只装一条**(旧名 `labelWingChunk`,多条一次,已废)。返回该条的 wing,
42
+ * 失败/字段缺失/值非法返回 null(记账归调用方,本函数不动全局状态)。
43
+ */
44
+ export declare function labelWingOne(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, rec: MemoryRecord, candidates: string, callTimeoutMs?: number): Promise<string | null>;
45
+ /**
46
+ * 涌现标签标注器(导出):为**一条**记录提炼 slug 标签(tags)——Room 的前身。
47
+ * 一个请求只装一条(旧名 `tagChunk`,多条一次,已废)。失败返回 null;合法但为空返回 []。
43
48
  */
44
- export declare function tagChunk(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, chunk: MemoryRecord[]): Promise<Array<{
45
- record: MemoryRecord;
46
- tags: string[];
47
- }>>;
49
+ export declare function tagOne(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, rec: MemoryRecord, callTimeoutMs?: number): Promise<string[] | null>;