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/stats.js
CHANGED
|
@@ -32,7 +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
|
+
import { startKeywordBackfill, keywordBackfillState } from './keyword-backfill.js';
|
|
37
|
+
import { startRedistillBackfill, redistillBackfillState } from './redistill-backfill.js';
|
|
36
38
|
import { errDetail } from './util/filelog.js';
|
|
37
39
|
import { snapshotTokenCost } from './token-cost.js';
|
|
38
40
|
import { computeMaturity } from './util/maturity.js';
|
|
@@ -41,13 +43,31 @@ import { relinkWorkspaces } from './pipeline/workspace-relink.js';
|
|
|
41
43
|
import { reconcileLedger } from './store/memory-md-export.js';
|
|
42
44
|
import { mdRunStatus, runProjection } from './store/memory-md-run.js';
|
|
43
45
|
import { disableProjectionFile } from './store/memory-md-disable.js';
|
|
44
|
-
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';
|
|
45
47
|
import { ensureTypeSkeleton } from './store/memory-md-skeleton.js';
|
|
46
48
|
import { sweepOrphanFiles } from './store/memory-md-orphan.js';
|
|
47
49
|
import { collectProjectionFiles } from './store/memory-md-archive.js';
|
|
48
50
|
import { syncFromFiles } from './pipeline/md-sync.js';
|
|
49
51
|
const require = createRequire(import.meta.url);
|
|
50
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
|
+
}
|
|
51
71
|
/**
|
|
52
72
|
* 端点全集运行时清单(与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
53
73
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
@@ -70,10 +90,13 @@ export const MEMORY_ENDPOINTS = [
|
|
|
70
90
|
'dsh-memory/room-register',
|
|
71
91
|
'dsh-memory/room-admin',
|
|
72
92
|
'dsh-memory/wing-backfill',
|
|
93
|
+
'dsh-memory/keyword-backfill',
|
|
94
|
+
'dsh-memory/redistill-backfill',
|
|
73
95
|
'dsh-memory/session-stats',
|
|
74
96
|
'dsh-memory/settings-get',
|
|
75
97
|
'dsh-memory/settings-set',
|
|
76
98
|
'dsh-memory/list-records',
|
|
99
|
+
'dsh-memory/records-meta',
|
|
77
100
|
'dsh-memory/records-delete',
|
|
78
101
|
'dsh-memory/receipts',
|
|
79
102
|
'dsh-memory/conflicts',
|
|
@@ -631,10 +654,11 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
631
654
|
case 'dsh-memory/mem-sync': {
|
|
632
655
|
// P2 反向同步:文件 → 库。与导出的**风险不对称**——导出只写文件,本项会写库,
|
|
633
656
|
// 所以多两道门:要先开投射(没导出过 = 账本为空 = 判不出“谁变了”),要高权限。
|
|
634
|
-
const
|
|
657
|
+
const ec = effectiveCfg(cfg, live);
|
|
658
|
+
const syn = ec.mdSync;
|
|
635
659
|
if (syn?.enabled !== true)
|
|
636
|
-
throw new Error('md
|
|
637
|
-
if (
|
|
660
|
+
throw new Error('md 反向同步未开启(概览页开关,或 config mdSync.enabled)');
|
|
661
|
+
if (ec.memExport?.enabled !== true)
|
|
638
662
|
throw new Error('md 反向同步需先开启 md 投射(cfg.memExport.enabled)');
|
|
639
663
|
if (!live?.get().memoryMutate)
|
|
640
664
|
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能回灌文件侧修改。');
|
|
@@ -792,7 +816,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
792
816
|
}
|
|
793
817
|
// ── 轨3 外部记忆来源(Connect):登记/列表/退役/只读扫描。整面默认关(cfg.memport.enabled)。
|
|
794
818
|
case 'dsh-memory/memory-sources-list': {
|
|
795
|
-
const enabled = cfg.memport?.enabled === true;
|
|
819
|
+
const enabled = effectiveCfg(cfg, live).memport?.enabled === true;
|
|
796
820
|
const v = {
|
|
797
821
|
sources: enabled && memorySources
|
|
798
822
|
? memorySources.list().map((e) => ({
|
|
@@ -811,8 +835,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
811
835
|
return v;
|
|
812
836
|
}
|
|
813
837
|
case 'dsh-memory/memory-sources-add': {
|
|
814
|
-
if (cfg.memport?.enabled !== true)
|
|
815
|
-
throw new Error('
|
|
838
|
+
if (effectiveCfg(cfg, live).memport?.enabled !== true)
|
|
839
|
+
throw new Error('外部记忆来源未启用(概览页开关,或 config memport.enabled)。');
|
|
816
840
|
if (!memorySources)
|
|
817
841
|
throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
|
|
818
842
|
if (!live?.get().memoryMutate)
|
|
@@ -832,8 +856,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
832
856
|
return v;
|
|
833
857
|
}
|
|
834
858
|
case 'dsh-memory/memory-sources-retire': {
|
|
835
|
-
if (cfg.memport?.enabled !== true)
|
|
836
|
-
throw new Error('
|
|
859
|
+
if (effectiveCfg(cfg, live).memport?.enabled !== true)
|
|
860
|
+
throw new Error('外部记忆来源未启用(概览页开关,或 config memport.enabled)。');
|
|
837
861
|
if (!memorySources)
|
|
838
862
|
throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
|
|
839
863
|
if (!live?.get().memoryMutate)
|
|
@@ -851,8 +875,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
851
875
|
return v;
|
|
852
876
|
}
|
|
853
877
|
case 'dsh-memory/memport-scan': {
|
|
854
|
-
if (cfg.memport?.enabled !== true) {
|
|
855
|
-
const v = { enabled: false, sources: [], notice: '
|
|
878
|
+
if (effectiveCfg(cfg, live).memport?.enabled !== true) {
|
|
879
|
+
const v = { enabled: false, sources: [], notice: '外部记忆扫描未启用(概览页开关,或 config memport.enabled)。' };
|
|
856
880
|
return v;
|
|
857
881
|
}
|
|
858
882
|
if (!memorySources) {
|
|
@@ -862,7 +886,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
862
886
|
const p = (payload ?? {});
|
|
863
887
|
const all = memorySources.listEnabled();
|
|
864
888
|
const targets = p.sourceId ? all.filter((e) => e.id === p.sourceId) : all;
|
|
865
|
-
const maxRecords = cfg.memport?.scanMaxRecords ?? 5000;
|
|
889
|
+
const maxRecords = effectiveCfg(cfg, live).memport?.scanMaxRecords ?? 5000;
|
|
866
890
|
// 只读扫描:不写库(写走 memory_import)。逐个标扫描时间(失败不影响预览)。
|
|
867
891
|
const sources = await scanSources({ sources: targets, l1: stores.l1, maxRecords });
|
|
868
892
|
const nowIso = new Date().toISOString();
|
|
@@ -1132,6 +1156,10 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1132
1156
|
return v;
|
|
1133
1157
|
}
|
|
1134
1158
|
case 'dsh-memory/wing-backfill': {
|
|
1159
|
+
// probe=true 只读快照:不启动(UI 离开区域再回来时恢复在途任务用)。
|
|
1160
|
+
if (payload?.probe === true) {
|
|
1161
|
+
return { started: false, running: wingBackfillState().running };
|
|
1162
|
+
}
|
|
1135
1163
|
const r = startWingBackfill({
|
|
1136
1164
|
ctx: deps.ctx,
|
|
1137
1165
|
cfg: deps.cfg,
|
|
@@ -1140,6 +1168,49 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1140
1168
|
});
|
|
1141
1169
|
return r;
|
|
1142
1170
|
}
|
|
1171
|
+
case 'dsh-memory/keyword-backfill': {
|
|
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) };
|
|
1181
|
+
const r = startKeywordBackfill({
|
|
1182
|
+
ctx: deps.ctx,
|
|
1183
|
+
cfg: ec,
|
|
1184
|
+
backend: stores.backend ?? new InProcMemoryBackend(stores.l1),
|
|
1185
|
+
logger: deps.logger,
|
|
1186
|
+
// 运行时并发读取器:拖概览页滑块对**正在跑**的任务即时生效(弹性池每完成一条重读)。
|
|
1187
|
+
concurrency: () => effectiveCfg(cfg, live).backfill?.concurrency,
|
|
1188
|
+
});
|
|
1189
|
+
const st = keywordBackfillState();
|
|
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) };
|
|
1213
|
+
}
|
|
1143
1214
|
// ── 会话级统计(悬浮卡信息区;热路径端点,见 SessionInfoSource 的零 I/O 硬规则) ──
|
|
1144
1215
|
case 'dsh-memory/session-stats': {
|
|
1145
1216
|
if (!sessionInfo)
|
|
@@ -1249,7 +1320,12 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1249
1320
|
memoryMutate: false,
|
|
1250
1321
|
conflictFreeze: live?.get()?.conflictFreeze === true,
|
|
1251
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,
|
|
1252
1327
|
mdExportMaxRecords: live?.get()?.mdExportMaxRecords ?? 0,
|
|
1328
|
+
backfillConcurrency: live?.get()?.backfillConcurrency ?? 0,
|
|
1253
1329
|
}),
|
|
1254
1330
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
1255
1331
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
@@ -1288,7 +1364,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1288
1364
|
const patch = (payload ?? {});
|
|
1289
1365
|
const clean = {};
|
|
1290
1366
|
// 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
|
|
1291
|
-
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']) {
|
|
1292
1368
|
if (typeof patch[key] === 'boolean')
|
|
1293
1369
|
clean[key] = patch[key];
|
|
1294
1370
|
}
|
|
@@ -1297,6 +1373,11 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1297
1373
|
if (typeof patch.mdExportMaxRecords === 'number' && Number.isFinite(patch.mdExportMaxRecords)) {
|
|
1298
1374
|
clean.mdExportMaxRecords = Math.min(10_000, Math.max(0, Math.floor(patch.mdExportMaxRecords)));
|
|
1299
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
|
+
}
|
|
1300
1381
|
// 运行时统一路由链:结构校验后整体写入(空数组 = 回到跟随部署配置)
|
|
1301
1382
|
if (patch.distillChain !== undefined) {
|
|
1302
1383
|
const err = validateDistillChain(patch.distillChain);
|
|
@@ -1615,6 +1696,37 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1615
1696
|
})),
|
|
1616
1697
|
};
|
|
1617
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
|
+
}
|
|
1618
1730
|
case 'dsh-memory/records-delete': {
|
|
1619
1731
|
// 面板高权限删除指定记忆;写入删权限门(memoryMutate)防御。
|
|
1620
1732
|
//
|
package/dist/store/l1.d.ts
CHANGED
|
@@ -122,11 +122,29 @@ 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 不存在或写入失败——调用方须记账,不许静默。
|
|
128
140
|
*/
|
|
129
141
|
patchMetadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* 写 metadata_json + 顺带重索引 `l1_fts`(关键词回填专用)。
|
|
144
|
+
* 与 `patchMetadata` 的区分:后者只改主表(适合 tags/room/relabel 这类不进 fts 的元数据);
|
|
145
|
+
* 本方法额外重跑该行的 fts 插入,让新写的 `dsh_keywords` 立刻进 BM25 关键词轴。
|
|
146
|
+
*/
|
|
147
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): boolean;
|
|
130
148
|
/** repo 归属修补(ADR-0015 T3.3;人工消歧用)。CAS:expectRepoKeyName 非空时要求当前值匹配。 */
|
|
131
149
|
patchRepoKey(id: string, patch: {
|
|
132
150
|
repoKeyName?: string;
|
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 不存在或写入失败——调用方须记账,不许静默。
|
|
@@ -156,6 +165,14 @@ export class L1Store {
|
|
|
156
165
|
patchMetadata(id, metadata) {
|
|
157
166
|
return this.db.patchL1Metadata(id, metadata);
|
|
158
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* 写 metadata_json + 顺带重索引 `l1_fts`(关键词回填专用)。
|
|
170
|
+
* 与 `patchMetadata` 的区分:后者只改主表(适合 tags/room/relabel 这类不进 fts 的元数据);
|
|
171
|
+
* 本方法额外重跑该行的 fts 插入,让新写的 `dsh_keywords` 立刻进 BM25 关键词轴。
|
|
172
|
+
*/
|
|
173
|
+
patchMetadataAndReindex(id, metadata) {
|
|
174
|
+
return this.db.patchL1MetadataAndReindex(id, metadata);
|
|
175
|
+
}
|
|
159
176
|
/** repo 归属修补(ADR-0015 T3.3;人工消歧用)。CAS:expectRepoKeyName 非空时要求当前值匹配。 */
|
|
160
177
|
patchRepoKey(id, patch, expectRepoKeyName) {
|
|
161
178
|
const changed = this.db.patchRepoKey(id, patch, expectRepoKeyName);
|
|
@@ -29,6 +29,7 @@ export declare class WorkerMemoryBackend implements MemoryBackend {
|
|
|
29
29
|
getByIds(ids: string[]): Promise<MemoryRecord[]>;
|
|
30
30
|
list(opts: L1ListOpts): Promise<L1ListResult>;
|
|
31
31
|
patchMetadata(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
32
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
32
33
|
size(): Promise<number>;
|
|
33
34
|
dispose(): Promise<void>;
|
|
34
35
|
}
|
|
@@ -100,6 +100,9 @@ export class WorkerMemoryBackend {
|
|
|
100
100
|
async patchMetadata(id, metadata) {
|
|
101
101
|
return this.call('patchMetadata', [id, metadata]);
|
|
102
102
|
}
|
|
103
|
+
async patchMetadataAndReindex(id, metadata) {
|
|
104
|
+
return this.call('patchMetadataAndReindex', [id, metadata]);
|
|
105
|
+
}
|
|
103
106
|
async size() {
|
|
104
107
|
return this.call('size', []);
|
|
105
108
|
}
|
|
@@ -50,6 +50,8 @@ export interface MemoryBackend {
|
|
|
50
50
|
list(opts: L1ListOpts): Promise<L1ListResult>;
|
|
51
51
|
/** **只**更新 metadata_json(不动 content,不重算嵌入)。 */
|
|
52
52
|
patchMetadata(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
53
|
+
/** 写 metadata_json + 重索引该行的 `l1_fts`(关键词回填专用;新 `dsh_keywords` 要立刻进 BM25 轴)。 */
|
|
54
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
53
55
|
/** L1 记录总数(巡检进度分母)。 */
|
|
54
56
|
size(): Promise<number>;
|
|
55
57
|
/** 释放后端(worker 实现会 terminate 线程)。 */
|
|
@@ -68,6 +70,7 @@ export declare class InProcMemoryBackend implements MemoryBackend {
|
|
|
68
70
|
getByIds(ids: string[]): Promise<MemoryRecord[]>;
|
|
69
71
|
list(opts: L1ListOpts): Promise<L1ListResult>;
|
|
70
72
|
patchMetadata(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
73
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
71
74
|
size(): Promise<number>;
|
|
72
75
|
dispose(): Promise<void>;
|
|
73
76
|
}
|
|
@@ -21,6 +21,9 @@ export class InProcMemoryBackend {
|
|
|
21
21
|
async patchMetadata(id, metadata) {
|
|
22
22
|
return this.l1.patchMetadata(id, metadata);
|
|
23
23
|
}
|
|
24
|
+
async patchMetadataAndReindex(id, metadata) {
|
|
25
|
+
return this.l1.patchMetadataAndReindex(id, metadata);
|
|
26
|
+
}
|
|
24
27
|
async size() {
|
|
25
28
|
return this.l1.size;
|
|
26
29
|
}
|
|
@@ -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
|
*
|
package/dist/store/memory-md.js
CHANGED
|
@@ -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
|
|
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
|
: [];
|
|
@@ -60,6 +60,9 @@ else {
|
|
|
60
60
|
case 'patchMetadata':
|
|
61
61
|
reply(id, true, handle.patchL1Metadata(String(args[0]), args[1]));
|
|
62
62
|
return;
|
|
63
|
+
case 'patchMetadataAndReindex':
|
|
64
|
+
reply(id, true, handle.patchL1MetadataAndReindex(String(args[0]), args[1]));
|
|
65
|
+
return;
|
|
63
66
|
case 'size':
|
|
64
67
|
reply(id, true, handle.countL1());
|
|
65
68
|
return;
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -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
|
*
|
|
@@ -305,6 +322,17 @@ export declare class MemoryDb {
|
|
|
305
322
|
/** 打标作业完成/失败(失败保留行供诊断;重算由下一次触发再入队)。 */
|
|
306
323
|
finishSceneRecluster(jobId: string, ok: boolean): void;
|
|
307
324
|
patchL1Metadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
325
|
+
/**
|
|
326
|
+
* 更新 metadata_json 后**顺带重索引该行的 `l1_fts`**(关键词回填专用)。
|
|
327
|
+
*
|
|
328
|
+
* `patchL1Metadata` 本身不碰 fts——多数写回点(tags / room / relabel)不需要、也不想付这个代价;
|
|
329
|
+
* 但关键词轴若只写元数据不重索引,`ftsKeywordText` 读不到新 `dsh_keywords`,那条 BM25 轴
|
|
330
|
+
* 对存量记录永远空。故把「写元数据 + 重索引」焊在一个方法里,杜绝“写了关键词但检索看不到”。
|
|
331
|
+
*
|
|
332
|
+
* 退场/不存在的行:只从 fts 删除(幂等),不插入。fts 不可用时降级为纯 `patchL1Metadata`。
|
|
333
|
+
* 参数列表与 `stmtL1FtsInsert` / `backfillL1Fts` 逐位对齐(同三处列清单,漏一列静默丢索引)。
|
|
334
|
+
*/
|
|
335
|
+
patchL1MetadataAndReindex(id: string, metadata: Record<string, unknown>): boolean;
|
|
308
336
|
/** 全量读取(调试/迁移/重嵌入用;检索请走 FTS/向量)。 */
|
|
309
337
|
getAllL1(): MemoryRecord[];
|
|
310
338
|
getL1ByIds(ids: string[]): MemoryRecord[];
|
package/dist/store/sqlite.js
CHANGED
|
@@ -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
|
*
|
|
@@ -1509,6 +1542,39 @@ export class MemoryDb {
|
|
|
1509
1542
|
return false;
|
|
1510
1543
|
}
|
|
1511
1544
|
}
|
|
1545
|
+
/**
|
|
1546
|
+
* 更新 metadata_json 后**顺带重索引该行的 `l1_fts`**(关键词回填专用)。
|
|
1547
|
+
*
|
|
1548
|
+
* `patchL1Metadata` 本身不碰 fts——多数写回点(tags / room / relabel)不需要、也不想付这个代价;
|
|
1549
|
+
* 但关键词轴若只写元数据不重索引,`ftsKeywordText` 读不到新 `dsh_keywords`,那条 BM25 轴
|
|
1550
|
+
* 对存量记录永远空。故把「写元数据 + 重索引」焊在一个方法里,杜绝“写了关键词但检索看不到”。
|
|
1551
|
+
*
|
|
1552
|
+
* 退场/不存在的行:只从 fts 删除(幂等),不插入。fts 不可用时降级为纯 `patchL1Metadata`。
|
|
1553
|
+
* 参数列表与 `stmtL1FtsInsert` / `backfillL1Fts` 逐位对齐(同三处列清单,漏一列静默丢索引)。
|
|
1554
|
+
*/
|
|
1555
|
+
patchL1MetadataAndReindex(id, metadata) {
|
|
1556
|
+
const ok = this.patchL1Metadata(id, metadata);
|
|
1557
|
+
if (!ok || this.degraded || !this.ftsAvailable)
|
|
1558
|
+
return ok;
|
|
1559
|
+
try {
|
|
1560
|
+
const r = this.db
|
|
1561
|
+
.prepare(`SELECT record_id, content, type, priority, scene_name, session_id, version,
|
|
1562
|
+
timestamp_str, timestamp_start, timestamp_end, metadata_json, family,
|
|
1563
|
+
scope, workspace_id FROM l1_records
|
|
1564
|
+
WHERE record_id = ? AND NOT (${retiredL1Sql()})`)
|
|
1565
|
+
.get(id);
|
|
1566
|
+
// 退场/缺失行:只确保从 fts 剔除(幂等),不插入;活跃行:删旧行再按当前(含新关键词)元数据重插。
|
|
1567
|
+
this.stmtL1FtsDelete.run(id);
|
|
1568
|
+
if (r) {
|
|
1569
|
+
this.stmtL1FtsInsert.run(tokenizeForFts(ftsKeywordText(parseMetadataJson(String(r.metadata_json ?? '{}')))), tokenizeForFts(String(r.content ?? '')), String(r.content ?? ''), String(r.record_id ?? ''), String(r.type ?? ''), Number(r.priority ?? 50), String(r.scene_name ?? ''), String(r.session_id ?? 'default'), Number(r.version ?? 0), String(r.timestamp_str ?? ''), String(r.timestamp_start ?? ''), String(r.timestamp_end ?? ''), String(r.metadata_json ?? '{}'), String(r.family ?? 'chat'), normScope(r.scope), String(r.workspace_id ?? ''));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
catch (err) {
|
|
1573
|
+
// 重索引失败不回滚元数据写(关键词已落主表);下次全量 FTS 重建会对齐,并留痕。
|
|
1574
|
+
this.logger?.warn(`${TAG} 关键词重索引失败(id=${id},元数据已写、fts 待重建对齐): ${err instanceof Error ? err.message : String(err)}`);
|
|
1575
|
+
}
|
|
1576
|
+
return ok;
|
|
1577
|
+
}
|
|
1512
1578
|
/** 全量读取(调试/迁移/重嵌入用;检索请走 FTS/向量)。 */
|
|
1513
1579
|
getAllL1() {
|
|
1514
1580
|
if (this.degraded)
|
package/dist/wing-backfill.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 存量未打标记录的一键回填(task_15):从 WingWheel"未打标 M"入口触发,
|
|
3
|
-
* 对 metadata 无 wing 的 L1 记录按 8
|
|
3
|
+
* 对 metadata 无 wing 的 L1 记录按 8 角词表**逐条**补打标签(复用 L1 抽取的打标路径:
|
|
4
4
|
* 同一候选词表与措辞,经既有蒸馏 LLM 路由)。
|
|
5
5
|
*
|
|
6
6
|
* 安全边界:
|
|
7
7
|
* - **单飞**:同时最多一个回填任务,重复触发直接返回 running(不排队不叠加);
|
|
8
|
-
* -
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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
|
|
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>;
|