dsh-prime-memory 0.26.5 → 0.26.9
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.md +39 -0
- package/README.en.md +62 -1
- package/README.md +60 -2
- package/dist/client.js +45 -5
- package/dist/config.d.ts +40 -0
- package/dist/config.js +33 -3
- package/dist/contract.d.ts +14 -1
- package/dist/embedding-child.cjs +192 -0
- package/dist/embedding-worker.cjs +2 -0
- package/dist/graph/apply.d.ts +9 -0
- package/dist/graph/apply.js +12 -2
- package/dist/graph/search.d.ts +11 -2
- package/dist/graph/search.js +38 -10
- package/dist/hooks/recall.js +28 -1
- package/dist/index.js +14 -1
- package/dist/llm.d.ts +21 -2
- package/dist/llm.js +56 -25
- package/dist/pipeline/graph.d.ts +17 -1
- package/dist/pipeline/graph.js +45 -2
- package/dist/pipeline/l1.d.ts +15 -2
- package/dist/pipeline/l1.js +161 -47
- package/dist/pipeline/l2.js +72 -50
- package/dist/pipeline/md-sync.js +4 -2
- package/dist/pipeline/ruminate.js +2 -0
- package/dist/pipeline/runner.d.ts +32 -0
- package/dist/pipeline/runner.js +109 -11
- package/dist/pipeline/slot-migrate.d.ts +3 -2
- package/dist/pipeline/slot-migrate.js +3 -2
- package/dist/projection/recall.d.ts +10 -3
- package/dist/projection/recall.js +14 -0
- package/dist/settings.js +6 -0
- package/dist/stats.d.ts +1 -1
- package/dist/stats.js +35 -14
- package/dist/store/embedding-source.d.ts +4 -2
- package/dist/store/embedding-source.js +5 -2
- package/dist/store/file-versions.d.ts +2 -0
- package/dist/store/file-versions.js +2 -0
- package/dist/store/graph-store.d.ts +71 -1
- package/dist/store/graph-store.js +221 -8
- package/dist/store/l1.d.ts +5 -0
- package/dist/store/l1.js +18 -2
- package/dist/store/local-embedding.d.ts +9 -5
- package/dist/store/local-embedding.js +137 -17
- package/dist/store/memory-md-disable.d.ts +3 -2
- package/dist/store/memory-md.d.ts +3 -3
- package/dist/store/memory-md.js +6 -4
- package/dist/store/route-health.d.ts +38 -0
- package/dist/store/route-health.js +115 -0
- package/dist/store/runtime-installer.d.ts +13 -1
- package/dist/store/runtime-installer.js +40 -2
- package/dist/store/sqlite.d.ts +24 -12
- package/dist/store/sqlite.js +97 -31
- package/dist/store/supersede.d.ts +10 -2
- package/dist/store/supersede.js +10 -2
- package/dist/store/validity.d.ts +150 -0
- package/dist/store/validity.js +209 -0
- package/dist/tools/index.d.ts +15 -1
- package/dist/tools/index.js +393 -11
- package/dist/util/json-repair.d.ts +48 -0
- package/dist/util/json-repair.js +285 -0
- package/dist/validity-backfill.d.ts +78 -0
- package/dist/validity-backfill.js +160 -0
- package/dsh.plugin.json +2 -2
- package/package.json +3 -3
package/dist/stats.js
CHANGED
|
@@ -26,7 +26,7 @@ import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/re
|
|
|
26
26
|
import { getTraceStore } from './store/trace.js';
|
|
27
27
|
import { resolveConflictPair, listConflictPairs } from './conflict-service.js';
|
|
28
28
|
import { sourceAnchorLabels } from './pipeline/anchors.js';
|
|
29
|
-
import { readSupersedeMarker } from './store/supersede.js';
|
|
29
|
+
import { readSupersedeMarker, withSupersedeMarker, isRetired } from './store/supersede.js';
|
|
30
30
|
import { isSnapshotName } from './store/l1-snapshot.js';
|
|
31
31
|
import { WING_CATALOG, WING_FALLBACK } from './types.js';
|
|
32
32
|
import { isRoomSlug } from './metadata-validators.js';
|
|
@@ -525,7 +525,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
525
525
|
let retired = false;
|
|
526
526
|
let announced = false;
|
|
527
527
|
// 退场**前**的记录快照(带 workspaceId/name)——失效联动要靠它算落点,
|
|
528
|
-
// 而 `retire`
|
|
528
|
+
// 而 `retire` 只写 marker,不会动这两列,所以取在退场前后等价,取在前更直白。
|
|
529
529
|
let rec;
|
|
530
530
|
if (p.score === -1) {
|
|
531
531
|
rec = stores.l1.getByIds([id])[0];
|
|
@@ -547,9 +547,11 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
547
547
|
// (面板无消费方),磁盘凭证的对账走 mem-reconcile。
|
|
548
548
|
if (retired && rec !== undefined && cfg.memExport?.enabled === true && stores.mdLedger !== undefined) {
|
|
549
549
|
void disableProjectionFile({
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
record: {
|
|
550
|
+
// 传退场**后**的快照:门1 解耦后 md 层按 marker 判退场,这里补上同样形状的
|
|
551
|
+
// 取代标记,渲染出的 frontmatter 才带 `retired: true`——凭证自己就能说清它为什么存在。
|
|
552
|
+
record: withSupersedeMarker(rec.metadata, { at: retiredAt, reason: 'obsolete' }) === rec.metadata
|
|
553
|
+
? rec
|
|
554
|
+
: { ...rec, metadata: withSupersedeMarker(rec.metadata, { at: retiredAt, reason: 'obsolete' }) },
|
|
553
555
|
cfgRoot: cfg.memExport.root,
|
|
554
556
|
dataDir: deps.dataDir,
|
|
555
557
|
ledger: stores.mdLedger,
|
|
@@ -1342,6 +1344,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1342
1344
|
toolRoom: true, toolGraph: true, toolRuminate: true, toolConflict: true, toolMutate: true, toolSkills: true,
|
|
1343
1345
|
reasoningEffort: '', distillProvider: '', distillModel: '', distillChain: [],
|
|
1344
1346
|
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0, graph: 0 }, distillMaxInputChars: 0,
|
|
1347
|
+
distillTimeoutMs: 0,
|
|
1345
1348
|
distillLayerChains: { l1: [], l2: [], l3: [] },
|
|
1346
1349
|
distillMode: '', directBaseURL: '', directApiKey: '',
|
|
1347
1350
|
embedRemoteBaseURL: '', embedRemoteApiKey: '', embedRemoteModel: '', embedRemoteDimensions: 0,
|
|
@@ -1352,6 +1355,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1352
1355
|
redistillEnabled: live?.get()?.redistillEnabled === true,
|
|
1353
1356
|
mdSyncEnabled: live?.get()?.mdSyncEnabled === true,
|
|
1354
1357
|
memportEnabled: live?.get()?.memportEnabled === true,
|
|
1358
|
+
validityTiering: live?.get()?.validityTiering === true,
|
|
1355
1359
|
mdExportMaxRecords: live?.get()?.mdExportMaxRecords ?? 0,
|
|
1356
1360
|
backfillConcurrency: live?.get()?.backfillConcurrency ?? 0,
|
|
1357
1361
|
}),
|
|
@@ -1383,6 +1387,12 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1383
1387
|
fallback: cfg.llm.maxInputChars,
|
|
1384
1388
|
effective: s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : cfg.llm.maxInputChars,
|
|
1385
1389
|
},
|
|
1390
|
+
// R10 蒸馏超时(ms):current 是运行时覆盖(0 = 跟随配置),fallback 是静态 llm.timeoutMs
|
|
1391
|
+
timeoutBudget: {
|
|
1392
|
+
current: s?.distillTimeoutMs ?? 0,
|
|
1393
|
+
fallback: cfg.llm.timeoutMs,
|
|
1394
|
+
effective: s && s.distillTimeoutMs > 0 ? s.distillTimeoutMs : cfg.llm.timeoutMs,
|
|
1395
|
+
},
|
|
1386
1396
|
};
|
|
1387
1397
|
return resp;
|
|
1388
1398
|
}
|
|
@@ -1465,14 +1475,22 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1465
1475
|
}
|
|
1466
1476
|
clean.distillBudgets = budgets;
|
|
1467
1477
|
}
|
|
1468
|
-
// 输入预算(字符):0 = 跟随静态配置;正值须落在静态 schema 同款范围(1000~
|
|
1478
|
+
// 输入预算(字符):0 = 跟随静态配置;正值须落在静态 schema 同款范围(1000~256k)
|
|
1469
1479
|
if (patch.distillMaxInputChars !== undefined) {
|
|
1470
1480
|
const n = Number(patch.distillMaxInputChars);
|
|
1471
|
-
if (!Number.isInteger(n) || n < 0 || n >
|
|
1472
|
-
throw new Error('distillMaxInputChars 须为 0 或 1000~
|
|
1481
|
+
if (!Number.isInteger(n) || n < 0 || n > 256_000 || (n > 0 && n < 1000)) {
|
|
1482
|
+
throw new Error('distillMaxInputChars 须为 0 或 1000~256000 的整数(0 = 跟随配置)');
|
|
1473
1483
|
}
|
|
1474
1484
|
clean.distillMaxInputChars = n;
|
|
1475
1485
|
}
|
|
1486
|
+
// R10 蒸馏超时(ms):0 = 跟随静态配置;正值 1000~600000(与 live schema 同上限)
|
|
1487
|
+
if (patch.distillTimeoutMs !== undefined) {
|
|
1488
|
+
const n = Number(patch.distillTimeoutMs);
|
|
1489
|
+
if (!Number.isInteger(n) || n < 0 || n > 600_000 || (n > 0 && n < 1000)) {
|
|
1490
|
+
throw new Error('distillTimeoutMs 须为 0 或 1000~600000 的整数(0 = 跟随配置)');
|
|
1491
|
+
}
|
|
1492
|
+
clean.distillTimeoutMs = n;
|
|
1493
|
+
}
|
|
1476
1494
|
// 蒸馏通道运行时覆盖(direct 解耦):mode 白名单;endpoint 可回显;apiKey 属机密——
|
|
1477
1495
|
// 写入后存 settings,但不回读到 UI、不落本条日志(下方日志先剔除后记录)
|
|
1478
1496
|
if (patch.distillMode !== undefined) {
|
|
@@ -1750,7 +1768,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1750
1768
|
...(Array.isArray(r.metadata?.dsh_keywords) ? { keywords: r.metadata.dsh_keywords.filter((k) => typeof k === 'string' && k !== '') } : {}),
|
|
1751
1769
|
...(Array.isArray(r.metadata?.dsh_usage_scenario) ? { usageScenario: r.metadata.dsh_usage_scenario } : {}),
|
|
1752
1770
|
filePath,
|
|
1753
|
-
|
|
1771
|
+
// 退场判据与库侧同源(门1:marker 单源,valid_to 只是现实有效期)。
|
|
1772
|
+
retired: isRetired(r),
|
|
1754
1773
|
};
|
|
1755
1774
|
});
|
|
1756
1775
|
return { items };
|
|
@@ -1828,8 +1847,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1828
1847
|
let targets;
|
|
1829
1848
|
if (explicit.length > 0) {
|
|
1830
1849
|
// 显式 id 也要**复核**是否真的处于已退场态:防止调用方用一个 id 列表
|
|
1831
|
-
// 把活动记忆绕过软删直接物理抹掉(那等于给了一条硬删后门)。
|
|
1832
|
-
targets = explicit.filter((id) => stores.l1.getByIds([id]).some((r) => r
|
|
1850
|
+
// 把活动记忆绕过软删直接物理抹掉(那等于给了一条硬删后门)。判据 marker 单源(门1)。
|
|
1851
|
+
targets = explicit.filter((id) => stores.l1.getByIds([id]).some((r) => isRetired(r)));
|
|
1833
1852
|
}
|
|
1834
1853
|
else {
|
|
1835
1854
|
targets = [];
|
|
@@ -1847,7 +1866,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1847
1866
|
id: r.id,
|
|
1848
1867
|
name: r.name ?? '',
|
|
1849
1868
|
type: r.type,
|
|
1850
|
-
|
|
1869
|
+
// 退场时刻:marker.at 优先;旧版 retire 写的 valid_to 作回退(存量行兼容)。
|
|
1870
|
+
retiredAt: readSupersedeMarker(r.metadata)?.at ?? (r.validTo === undefined ? '' : new Date(r.validTo).toISOString()),
|
|
1851
1871
|
}));
|
|
1852
1872
|
const resp = {
|
|
1853
1873
|
dryRun: true,
|
|
@@ -1893,7 +1913,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1893
1913
|
id: x.id,
|
|
1894
1914
|
name: x.name ?? '',
|
|
1895
1915
|
type: x.type,
|
|
1896
|
-
retiredAt: x.validTo === undefined ? '' : new Date(x.validTo).toISOString(),
|
|
1916
|
+
retiredAt: readSupersedeMarker(x.metadata)?.at ?? (x.validTo === undefined ? '' : new Date(x.validTo).toISOString()),
|
|
1897
1917
|
})),
|
|
1898
1918
|
};
|
|
1899
1919
|
return resp;
|
|
@@ -2351,7 +2371,8 @@ export function agingWeightOf(updatedAtMs, halfLifeDays, now = Date.now()) {
|
|
|
2351
2371
|
return Math.max(0.5, Math.pow(0.5, ageDays / halfLifeDays));
|
|
2352
2372
|
}
|
|
2353
2373
|
export function hitToUiRecord(r, extra) {
|
|
2354
|
-
|
|
2374
|
+
// 退场判据 marker 单源(门1 解耦,与 `isRetired`/`retiredL1Sql` 同口径)。
|
|
2375
|
+
const retired = isRetired(r);
|
|
2355
2376
|
const mark = retired ? readSupersedeMarker(r.metadata) : undefined;
|
|
2356
2377
|
return {
|
|
2357
2378
|
id: r.id,
|
|
@@ -63,8 +63,10 @@ export interface InitialEmbedding {
|
|
|
63
63
|
export declare function remoteCeiling(cfg: MemoryConfig): boolean;
|
|
64
64
|
export declare function resolveInitialEmbedding(cfg: MemoryConfig, sourceStore: EmbeddingSourceStore, downloader: ModelDownloadQueue, makeLocal: (modelId: string) => LocalEmbeddingService | null, logger?: MemoryLogger): Promise<InitialEmbedding>;
|
|
65
65
|
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。
|
|
66
|
-
* 推理在
|
|
67
|
-
export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger, maxInputChars?: number
|
|
66
|
+
* 推理在 fork 子进程(见 local-embedding.ts,fix #11);此处只传 runtime 目录与模型目录。 */
|
|
67
|
+
export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger, maxInputChars?: number,
|
|
68
|
+
/** 测试注入缝:子进程资产路径(源码态下 dist 产物不存在,显式指向 resources 源文件)。 */
|
|
69
|
+
childPath?: string): (modelId: string) => LocalEmbeddingService | null;
|
|
68
70
|
export interface EmbeddingManagerDeps {
|
|
69
71
|
dataDir: string;
|
|
70
72
|
cfg: MemoryConfig;
|
|
@@ -153,14 +153,17 @@ export async function resolveInitialEmbedding(cfg, sourceStore, downloader, make
|
|
|
153
153
|
return { svc, dims: cfg.embedding.dimensions, providerInfo: svc.getProviderInfo() };
|
|
154
154
|
}
|
|
155
155
|
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。
|
|
156
|
-
* 推理在
|
|
157
|
-
export function makeLocalServiceFactory(installer, downloader, logger, maxInputChars
|
|
156
|
+
* 推理在 fork 子进程(见 local-embedding.ts,fix #11);此处只传 runtime 目录与模型目录。 */
|
|
157
|
+
export function makeLocalServiceFactory(installer, downloader, logger, maxInputChars,
|
|
158
|
+
/** 测试注入缝:子进程资产路径(源码态下 dist 产物不存在,显式指向 resources 源文件)。 */
|
|
159
|
+
childPath) {
|
|
158
160
|
return (modelId) => {
|
|
159
161
|
const entry = catalogById(modelId);
|
|
160
162
|
if (!entry)
|
|
161
163
|
return null;
|
|
162
164
|
return new LocalEmbeddingService(entry, downloader.modelsDir(entry.id), {
|
|
163
165
|
runtimeDir: installer.runtimeDir,
|
|
166
|
+
childPath,
|
|
164
167
|
logger,
|
|
165
168
|
maxInputChars,
|
|
166
169
|
});
|
|
@@ -29,3 +29,5 @@ export declare const PENDING_FILE_VERSION = 1;
|
|
|
29
29
|
export declare const RECONCILE_STATE_FILE_VERSION = 1;
|
|
30
30
|
/** `mem-md-ledger.json`(md 投射指纹账本,N9;派生态,删了可重建)。*/
|
|
31
31
|
export declare const MEM_MD_LEDGER_FILE_VERSION = 1;
|
|
32
|
+
/** `distill-route-health.json`(蒸馏路由健康学习值,R9;运行时派生,删了可重学)。*/
|
|
33
|
+
export declare const ROUTE_HEALTH_FILE_VERSION = 1;
|
|
@@ -29,3 +29,5 @@ export const PENDING_FILE_VERSION = 1;
|
|
|
29
29
|
export const RECONCILE_STATE_FILE_VERSION = 1;
|
|
30
30
|
/** `mem-md-ledger.json`(md 投射指纹账本,N9;派生态,删了可重建)。*/
|
|
31
31
|
export const MEM_MD_LEDGER_FILE_VERSION = 1;
|
|
32
|
+
/** `distill-route-health.json`(蒸馏路由健康学习值,R9;运行时派生,删了可重学)。*/
|
|
33
|
+
export const ROUTE_HEALTH_FILE_VERSION = 1;
|
|
@@ -139,6 +139,54 @@ export declare class GraphStore {
|
|
|
139
139
|
* 记录跳过。返回实际新建的 job 数。
|
|
140
140
|
*/
|
|
141
141
|
queueGraphProjection(recordIds: readonly string[], priority: number): number;
|
|
142
|
+
/**
|
|
143
|
+
* 入队的详细形态(与 `queueGraphProjection` 同一实现,只是把新建 job id 也交出来):
|
|
144
|
+
* `memory_reproject` 工具面需要 `{queued, jobIds}` 回报,而既有调用方只数 created——
|
|
145
|
+
* 两个返回形状共享一份去重/分片逻辑,不漂移。
|
|
146
|
+
*/
|
|
147
|
+
private queueGraphProjectionDetailed;
|
|
148
|
+
/**
|
|
149
|
+
* 强制重投(计划 expiry-tiering R10 冻结契约 · 先行刀):
|
|
150
|
+
* 抹掉 `graph_projected_records` 里这些记录的"已按当前版本投过"凭证,再按
|
|
151
|
+
* 新蒸馏优先级入队——绕开 `projector_version` 的永久跳过,让"改了 L1 → 图谱跟随"
|
|
152
|
+
* 有一条不依赖重启/周期 sweep 的纠正通道。
|
|
153
|
+
*
|
|
154
|
+
* **不动在途 `graph_job_records` 映射**(与上游 task_7.5 字面的一处刻意偏差,
|
|
155
|
+
* 理由记入写面计划 findings):claim 在认领时才现读 L1 记录,在途 job 天然投的是
|
|
156
|
+
* **改后**内容;若把映射一并删掉,同一次调用稍后再来一次就会绕过 PK 去重,
|
|
157
|
+
* 堆出内容重叠的双 job——保留映射才是"重复调用不堆重 job"的构造性保证。
|
|
158
|
+
*
|
|
159
|
+
* 幂等:在途有 job → blocked 集吸收,`created=0`;job 已完成 → 凭证刚被
|
|
160
|
+
* `complete` 写回,本方法再删再投,是合法的新重投而非堆积。
|
|
161
|
+
*/
|
|
162
|
+
forceReprojectRecord(recordIds: readonly string[]): {
|
|
163
|
+
queued: number;
|
|
164
|
+
jobIds: string[];
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* 确定性边提案落库面(graph-write-plane R4 兜底支路):把"工具传入的显式边提案"
|
|
168
|
+
* 当作一次确定性投影输入,经 `applyGraphProjection` **纯函数**校验后在同一事务内
|
|
169
|
+
* 写回——不是裸 UPDATE graph_edges:validSources 硬校验、同 (from,relation) 换目标
|
|
170
|
+
* 闭旧边的确定性支路、来源并集封顶,全部复用既有守门代码(R5 不变量由它保证)。
|
|
171
|
+
*
|
|
172
|
+
* 提案形态:两端节点按现有 name/type 重提(消歧合并会命中现有节点,不新铸实体),
|
|
173
|
+
* 边 (from → to, relation) 携调用方给的 sourceRecordIds。claim 同款现读 L1:
|
|
174
|
+
* allowedRecordIds = 命中的来源记录集,未命中的 id 会被纯函数整条拒掉(返回
|
|
175
|
+
* ok:false, reason='dropped')——工具面传的"非现存 L1"在这里撞上硬校验,而非静默落库。
|
|
176
|
+
*/
|
|
177
|
+
applyRelationProposal(opts: {
|
|
178
|
+
fromNodeId: string;
|
|
179
|
+
toNodeId: string;
|
|
180
|
+
relation: string;
|
|
181
|
+
sourceRecordIds: readonly string[];
|
|
182
|
+
now?: string;
|
|
183
|
+
}): {
|
|
184
|
+
ok: boolean;
|
|
185
|
+
reason?: string;
|
|
186
|
+
closedEdgeIds: string[];
|
|
187
|
+
edgeIds: string[];
|
|
188
|
+
touchedNodeIds: string[];
|
|
189
|
+
};
|
|
142
190
|
/**
|
|
143
191
|
* 记录被物理删除后,把两张**索引表**里指向它的行一并抹掉。
|
|
144
192
|
*
|
|
@@ -164,8 +212,11 @@ export declare class GraphStore {
|
|
|
164
212
|
* 校验)→ 写回 touched 行 → job 置 completed + 登记已投影记录 + 放掉 mapping。
|
|
165
213
|
* job 非 running 态(已完成/已 dead/已回收)一律幂等 no-op;任何一步抛错整体
|
|
166
214
|
* 回滚,不留半写。
|
|
215
|
+
*
|
|
216
|
+
* 返回 touched 节点中 active/disputed 的那批(写面向量收尾的输入,G-vec 缺陷修):
|
|
217
|
+
* 调用方只关心"哪些节点刚被写活",向量补算失败不回滚投影——返回值不参与控制流。
|
|
167
218
|
*/
|
|
168
|
-
complete(jobId: string, result: GraphProjectionResult, opts?: CompleteOptions):
|
|
219
|
+
complete(jobId: string, result: GraphProjectionResult, opts?: CompleteOptions): GraphNode[];
|
|
169
220
|
/**
|
|
170
221
|
* 投影失败收尾:attempts 已在 claim 时 +1——根据错误分类选不同封顶:
|
|
171
222
|
* - 永久错误(校验/解析):attempts >= GRAPH_JOB_MAX_ATTEMPTS(3) → dead
|
|
@@ -191,6 +242,25 @@ export declare class GraphStore {
|
|
|
191
242
|
* 之后;按 L1 存活集判定(而非本次删除集合),跨多次删除与历史孤儿一并收敛。
|
|
192
243
|
*/
|
|
193
244
|
markSourcesDeleted(deletedIds: readonly string[]): void;
|
|
245
|
+
/**
|
|
246
|
+
* 退场传播(计划 expiry-tiering R9 冻结契约 · 先行刀):软删/取代后的图谱
|
|
247
|
+
* 来源活性重算清扫。
|
|
248
|
+
*
|
|
249
|
+
* 与 `markSourcesDeleted` 的分野:那边判"来源 row 是否还活着",对软删(主表行
|
|
250
|
+
* 一直在)结构性失效;这边判"来源是否全部处于**退场态**"——即
|
|
251
|
+
* `classifyValidity` ∈ {superseded, retired}。退场判据单源在 validity 层,
|
|
252
|
+
* 本方不再自写一份 valid_to/marker 比较(写面计划约束 §4)。
|
|
253
|
+
*
|
|
254
|
+
* 重算式 sweep(不是逐条事件改写):每次按当前 L1 全量判定候选节点/边,
|
|
255
|
+
* 双向收敛——
|
|
256
|
+
* - active/disputed 且来源全部退场(含 row 消失)→ `archived` 墓碑(保留行,
|
|
257
|
+
* expand 可见,与物理删传播同落库形态);
|
|
258
|
+
* - archived 且来源全部复活(row 存在且非退场态)→ 回 `active`(restore 的回药;
|
|
259
|
+
* 被物理删清空来源的永不满 足此条,不会误复活)。
|
|
260
|
+
*
|
|
261
|
+
* `changedIds` 只作早退与诊断日志的口径,不参与判定(判定面 = 全图来源集)。
|
|
262
|
+
*/
|
|
263
|
+
markSourcesRetired(changedIds: readonly string[]): void;
|
|
194
264
|
/**
|
|
195
265
|
* §C 矛盾冻结:把图谱的 `disputed` 状态**同步**到给定冲突集。
|
|
196
266
|
*
|
|
@@ -21,8 +21,10 @@ import { randomBytes } from 'node:crypto';
|
|
|
21
21
|
import { applyGraphProjection } from '../graph/apply.js';
|
|
22
22
|
import { expandHitRecordIds, searchGraphNodes } from '../graph/search.js';
|
|
23
23
|
import { buildFullView, buildGraphPreview } from '../graph/preview.js';
|
|
24
|
-
import { GRAPH_JOB_BACKOFF_BASE_MS, GRAPH_JOB_BATCH, GRAPH_JOB_MAX_ATTEMPTS, GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS, GRAPH_PRIORITY_BACKFILL, GRAPH_PROJECTOR_VERSION, GRAPH_VEC_SEED_CAP, } from '../graph/types.js';
|
|
24
|
+
import { GRAPH_JOB_BACKOFF_BASE_MS, GRAPH_JOB_BATCH, GRAPH_JOB_MAX_ATTEMPTS, GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS, GRAPH_PRIORITY_BACKFILL, GRAPH_PRIORITY_NEW, GRAPH_PROJECTOR_VERSION, GRAPH_VEC_SEED_CAP, } from '../graph/types.js';
|
|
25
25
|
import { normPersistence } from '../types.js';
|
|
26
|
+
import { classifyValidity, isExitedState, validToSourceOf } from './validity.js';
|
|
27
|
+
import { readSupersedeMarker } from './supersede.js';
|
|
26
28
|
import { isZeroVector, vecToBuffer } from './vec-utils.js';
|
|
27
29
|
const TAG = '[memory][graph]';
|
|
28
30
|
/** 坏 JSON 容忍解析:图谱表列损坏只损失该行派生信息,不抛。 */
|
|
@@ -565,8 +567,16 @@ export class GraphStore {
|
|
|
565
567
|
* 记录跳过。返回实际新建的 job 数。
|
|
566
568
|
*/
|
|
567
569
|
queueGraphProjection(recordIds, priority) {
|
|
570
|
+
return this.queueGraphProjectionDetailed(recordIds, priority).created;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* 入队的详细形态(与 `queueGraphProjection` 同一实现,只是把新建 job id 也交出来):
|
|
574
|
+
* `memory_reproject` 工具面需要 `{queued, jobIds}` 回报,而既有调用方只数 created——
|
|
575
|
+
* 两个返回形状共享一份去重/分片逻辑,不漂移。
|
|
576
|
+
*/
|
|
577
|
+
queueGraphProjectionDetailed(recordIds, priority) {
|
|
568
578
|
if (!this.db || recordIds.length === 0)
|
|
569
|
-
return 0;
|
|
579
|
+
return { created: 0, jobIds: [] };
|
|
570
580
|
try {
|
|
571
581
|
const db = this.db;
|
|
572
582
|
const ids = [...new Set(recordIds)];
|
|
@@ -585,10 +595,10 @@ export class GraphStore {
|
|
|
585
595
|
}
|
|
586
596
|
const fresh = ids.filter((id) => !blocked.has(id));
|
|
587
597
|
if (fresh.length === 0)
|
|
588
|
-
return 0;
|
|
598
|
+
return { created: 0, jobIds: [] };
|
|
589
599
|
const now = new Date().toISOString();
|
|
590
600
|
return this.tx(() => {
|
|
591
|
-
|
|
601
|
+
const jobIds = [];
|
|
592
602
|
for (let i = 0; i < fresh.length; i += GRAPH_JOB_BATCH) {
|
|
593
603
|
const batch = fresh.slice(i, i + GRAPH_JOB_BATCH);
|
|
594
604
|
const jobId = this.newJobId();
|
|
@@ -597,14 +607,121 @@ export class GraphStore {
|
|
|
597
607
|
for (const id of batch) {
|
|
598
608
|
db.prepare('INSERT OR REPLACE INTO graph_job_records (record_id, job_id) VALUES (?, ?)').run(id, jobId);
|
|
599
609
|
}
|
|
600
|
-
|
|
610
|
+
jobIds.push(jobId);
|
|
601
611
|
}
|
|
602
|
-
return created;
|
|
612
|
+
return { created: jobIds.length, jobIds };
|
|
603
613
|
});
|
|
604
614
|
}
|
|
605
615
|
catch (err) {
|
|
606
616
|
this.logger?.warn(`${TAG} 投影入队失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
607
|
-
return 0;
|
|
617
|
+
return { created: 0, jobIds: [] };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* 强制重投(计划 expiry-tiering R10 冻结契约 · 先行刀):
|
|
622
|
+
* 抹掉 `graph_projected_records` 里这些记录的"已按当前版本投过"凭证,再按
|
|
623
|
+
* 新蒸馏优先级入队——绕开 `projector_version` 的永久跳过,让"改了 L1 → 图谱跟随"
|
|
624
|
+
* 有一条不依赖重启/周期 sweep 的纠正通道。
|
|
625
|
+
*
|
|
626
|
+
* **不动在途 `graph_job_records` 映射**(与上游 task_7.5 字面的一处刻意偏差,
|
|
627
|
+
* 理由记入写面计划 findings):claim 在认领时才现读 L1 记录,在途 job 天然投的是
|
|
628
|
+
* **改后**内容;若把映射一并删掉,同一次调用稍后再来一次就会绕过 PK 去重,
|
|
629
|
+
* 堆出内容重叠的双 job——保留映射才是"重复调用不堆重 job"的构造性保证。
|
|
630
|
+
*
|
|
631
|
+
* 幂等:在途有 job → blocked 集吸收,`created=0`;job 已完成 → 凭证刚被
|
|
632
|
+
* `complete` 写回,本方法再删再投,是合法的新重投而非堆积。
|
|
633
|
+
*/
|
|
634
|
+
forceReprojectRecord(recordIds) {
|
|
635
|
+
if (!this.db || recordIds.length === 0)
|
|
636
|
+
return { queued: 0, jobIds: [] };
|
|
637
|
+
const ids = [...new Set(recordIds)];
|
|
638
|
+
try {
|
|
639
|
+
this.tx(() => {
|
|
640
|
+
for (const chunk of chunkOf(ids)) {
|
|
641
|
+
const ph = chunk.map(() => '?').join(',');
|
|
642
|
+
this.db.prepare(`DELETE FROM graph_projected_records WHERE record_id IN (${ph})`).run(...chunk);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
const r = this.queueGraphProjectionDetailed(ids, GRAPH_PRIORITY_NEW);
|
|
646
|
+
return { queued: r.created, jobIds: r.jobIds };
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
this.logger?.warn(`${TAG} 强制重投入队失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
650
|
+
return { queued: 0, jobIds: [] };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* 确定性边提案落库面(graph-write-plane R4 兜底支路):把"工具传入的显式边提案"
|
|
655
|
+
* 当作一次确定性投影输入,经 `applyGraphProjection` **纯函数**校验后在同一事务内
|
|
656
|
+
* 写回——不是裸 UPDATE graph_edges:validSources 硬校验、同 (from,relation) 换目标
|
|
657
|
+
* 闭旧边的确定性支路、来源并集封顶,全部复用既有守门代码(R5 不变量由它保证)。
|
|
658
|
+
*
|
|
659
|
+
* 提案形态:两端节点按现有 name/type 重提(消歧合并会命中现有节点,不新铸实体),
|
|
660
|
+
* 边 (from → to, relation) 携调用方给的 sourceRecordIds。claim 同款现读 L1:
|
|
661
|
+
* allowedRecordIds = 命中的来源记录集,未命中的 id 会被纯函数整条拒掉(返回
|
|
662
|
+
* ok:false, reason='dropped')——工具面传的"非现存 L1"在这里撞上硬校验,而非静默落库。
|
|
663
|
+
*/
|
|
664
|
+
applyRelationProposal(opts) {
|
|
665
|
+
const fail = (reason) => ({ ok: false, reason, closedEdgeIds: [], edgeIds: [], touchedNodeIds: [] });
|
|
666
|
+
if (!this.db)
|
|
667
|
+
return fail('graph-not-ready');
|
|
668
|
+
const relation = String(opts.relation ?? '').trim();
|
|
669
|
+
const sourceIds = [...new Set((opts.sourceRecordIds ?? []).map((s) => String(s).trim()).filter((s) => s.length > 0))];
|
|
670
|
+
if (!opts.fromNodeId || !opts.toNodeId || opts.fromNodeId === opts.toNodeId)
|
|
671
|
+
return fail('bad-nodes');
|
|
672
|
+
if (!relation || sourceIds.length === 0)
|
|
673
|
+
return fail('empty-relation-or-sources');
|
|
674
|
+
try {
|
|
675
|
+
return this.tx(() => {
|
|
676
|
+
const { nodes, edges } = this.loadGraphInTx();
|
|
677
|
+
const from = nodes.find((n) => n.id === opts.fromNodeId);
|
|
678
|
+
const to = nodes.find((n) => n.id === opts.toNodeId);
|
|
679
|
+
if (!from || !to)
|
|
680
|
+
return fail('nodes-not-found');
|
|
681
|
+
const records = this.loadRecordsInTx(sourceIds);
|
|
682
|
+
if (records.length === 0)
|
|
683
|
+
return fail('sources-not-in-l1');
|
|
684
|
+
const now = opts.now ?? new Date().toISOString();
|
|
685
|
+
const idFactory = (prefix) => `${prefix}_${Date.now()}_${randomBytes(3).toString('hex')}`;
|
|
686
|
+
const result = {
|
|
687
|
+
reason: 'tool-explicit-proposal',
|
|
688
|
+
nodes: [
|
|
689
|
+
{ ref: 'from', name: from.name, type: from.type, sourceRecordIds: sourceIds, status: 'active' },
|
|
690
|
+
{ ref: 'to', name: to.name, type: to.type, sourceRecordIds: sourceIds, status: 'active' },
|
|
691
|
+
],
|
|
692
|
+
edges: [{ fromRef: 'from', toRef: 'to', relation, sourceRecordIds: sourceIds, status: 'active' }],
|
|
693
|
+
};
|
|
694
|
+
const statusBefore = new Map(edges.map((e) => [e.id, e.status]));
|
|
695
|
+
const outcome = applyGraphProjection({
|
|
696
|
+
nodes,
|
|
697
|
+
edges,
|
|
698
|
+
records,
|
|
699
|
+
result,
|
|
700
|
+
allowedRecordIds: new Set(records.map((r) => r.id)),
|
|
701
|
+
now,
|
|
702
|
+
idFactory,
|
|
703
|
+
});
|
|
704
|
+
if (outcome.edgeIds.length === 0)
|
|
705
|
+
return fail('proposal-dropped-by-validator');
|
|
706
|
+
const touchedNodes = nodes.filter((n) => outcome.nodeIds.includes(n.id));
|
|
707
|
+
const touchedEdges = edges.filter((e) => outcome.edgeIds.includes(e.id));
|
|
708
|
+
this.upsertTouchedInTx(touchedNodes, touchedEdges);
|
|
709
|
+
const closedEdgeIds = touchedEdges
|
|
710
|
+
.filter((e) => e.status === 'superseded' && statusBefore.get(e.id) !== 'superseded')
|
|
711
|
+
.map((e) => e.id);
|
|
712
|
+
const newEdgeIds = touchedEdges.filter((e) => e.status === 'active' && e.relation === relation).map((e) => e.id);
|
|
713
|
+
this.logger?.info(`${TAG} 确定性边提案落库:${newEdgeIds.length} 边 active,${closedEdgeIds.length} 旧边 superseded(来源 ${sourceIds.length} 条,校验丢弃 ${outcome.dropped} 项)`);
|
|
714
|
+
return {
|
|
715
|
+
ok: true,
|
|
716
|
+
closedEdgeIds,
|
|
717
|
+
edgeIds: newEdgeIds,
|
|
718
|
+
touchedNodeIds: touchedNodes.map((n) => n.id),
|
|
719
|
+
};
|
|
720
|
+
}, true);
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
this.logger?.warn(`${TAG} 确定性边提案失败(整体回滚): ${err instanceof Error ? err.message : String(err)}`);
|
|
724
|
+
return fail('tx-error');
|
|
608
725
|
}
|
|
609
726
|
}
|
|
610
727
|
/**
|
|
@@ -692,10 +809,14 @@ export class GraphStore {
|
|
|
692
809
|
* 校验)→ 写回 touched 行 → job 置 completed + 登记已投影记录 + 放掉 mapping。
|
|
693
810
|
* job 非 running 态(已完成/已 dead/已回收)一律幂等 no-op;任何一步抛错整体
|
|
694
811
|
* 回滚,不留半写。
|
|
812
|
+
*
|
|
813
|
+
* 返回 touched 节点中 active/disputed 的那批(写面向量收尾的输入,G-vec 缺陷修):
|
|
814
|
+
* 调用方只关心"哪些节点刚被写活",向量补算失败不回滚投影——返回值不参与控制流。
|
|
695
815
|
*/
|
|
696
816
|
complete(jobId, result, opts) {
|
|
697
817
|
if (!this.db)
|
|
698
|
-
return;
|
|
818
|
+
return [];
|
|
819
|
+
let touchedVecNodes = [];
|
|
699
820
|
try {
|
|
700
821
|
const db = this.db;
|
|
701
822
|
const now = opts?.now ?? new Date().toISOString();
|
|
@@ -721,6 +842,9 @@ export class GraphStore {
|
|
|
721
842
|
const touchedNodes = nodes.filter((n) => outcome.nodeIds.includes(n.id));
|
|
722
843
|
const touchedEdges = edges.filter((e) => outcome.edgeIds.includes(e.id));
|
|
723
844
|
this.upsertTouchedInTx(touchedNodes, touchedEdges);
|
|
845
|
+
// 写面收尾的输入(只递交落库后的 active/disputed 节点):墓碑不需要向量,
|
|
846
|
+
// 读侧 searchNodesByVector 本就按状态闸过滤陈旧向量行。
|
|
847
|
+
touchedVecNodes = touchedNodes.filter((n) => n.status === 'active' || n.status === 'disputed');
|
|
724
848
|
db.prepare(`UPDATE graph_projection_jobs SET status = 'completed', error = NULL, updated_time = ? WHERE job_id = ?`).run(now, jobId);
|
|
725
849
|
for (const id of wanted) {
|
|
726
850
|
db.prepare('INSERT OR REPLACE INTO graph_projected_records (record_id, projector_version) VALUES (?, ?)').run(id, GRAPH_PROJECTOR_VERSION);
|
|
@@ -736,6 +860,7 @@ export class GraphStore {
|
|
|
736
860
|
// 提交失败:job 仍在 running 态,下次启动 recoverRunning 回收重投
|
|
737
861
|
this.logger?.warn(`${TAG} 投影提交失败(待启动回收重投): ${err instanceof Error ? err.message : String(err)}`);
|
|
738
862
|
}
|
|
863
|
+
return touchedVecNodes;
|
|
739
864
|
}
|
|
740
865
|
/**
|
|
741
866
|
* 投影失败收尾:attempts 已在 claim 时 +1——根据错误分类选不同封顶:
|
|
@@ -874,6 +999,94 @@ export class GraphStore {
|
|
|
874
999
|
this.logger?.warn(`${TAG} 删除传播失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
875
1000
|
}
|
|
876
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* 退场传播(计划 expiry-tiering R9 冻结契约 · 先行刀):软删/取代后的图谱
|
|
1004
|
+
* 来源活性重算清扫。
|
|
1005
|
+
*
|
|
1006
|
+
* 与 `markSourcesDeleted` 的分野:那边判"来源 row 是否还活着",对软删(主表行
|
|
1007
|
+
* 一直在)结构性失效;这边判"来源是否全部处于**退场态**"——即
|
|
1008
|
+
* `classifyValidity` ∈ {superseded, retired}。退场判据单源在 validity 层,
|
|
1009
|
+
* 本方不再自写一份 valid_to/marker 比较(写面计划约束 §4)。
|
|
1010
|
+
*
|
|
1011
|
+
* 重算式 sweep(不是逐条事件改写):每次按当前 L1 全量判定候选节点/边,
|
|
1012
|
+
* 双向收敛——
|
|
1013
|
+
* - active/disputed 且来源全部退场(含 row 消失)→ `archived` 墓碑(保留行,
|
|
1014
|
+
* expand 可见,与物理删传播同落库形态);
|
|
1015
|
+
* - archived 且来源全部复活(row 存在且非退场态)→ 回 `active`(restore 的回药;
|
|
1016
|
+
* 被物理删清空来源的永不满 足此条,不会误复活)。
|
|
1017
|
+
*
|
|
1018
|
+
* `changedIds` 只作早退与诊断日志的口径,不参与判定(判定面 = 全图来源集)。
|
|
1019
|
+
*/
|
|
1020
|
+
markSourcesRetired(changedIds) {
|
|
1021
|
+
if (!this.db || changedIds.length === 0)
|
|
1022
|
+
return;
|
|
1023
|
+
try {
|
|
1024
|
+
const { nodes, edges } = this.loadGraph();
|
|
1025
|
+
const sourceIds = new Set();
|
|
1026
|
+
for (const n of nodes)
|
|
1027
|
+
for (const id of n.sourceRecordIds)
|
|
1028
|
+
sourceIds.add(id);
|
|
1029
|
+
for (const e of edges)
|
|
1030
|
+
for (const id of e.sourceRecordIds)
|
|
1031
|
+
sourceIds.add(id);
|
|
1032
|
+
if (sourceIds.size === 0)
|
|
1033
|
+
return;
|
|
1034
|
+
// 现读 L1 行(含已退场软删行——主表行一直在,差别在 marker);
|
|
1035
|
+
// 列形状按 l1SelectCols 同款容缺(独立测试夹具/旧库可能没有时间增强列)。
|
|
1036
|
+
const now = Date.now();
|
|
1037
|
+
const byId = new Map();
|
|
1038
|
+
for (const chunk of chunkOf([...sourceIds])) {
|
|
1039
|
+
const ph = chunk.map(() => '?').join(',');
|
|
1040
|
+
for (const r of this.db
|
|
1041
|
+
.prepare(`SELECT ${l1SelectCols(this.db)} FROM l1_records WHERE record_id IN (${ph})`)
|
|
1042
|
+
.all(...chunk)) {
|
|
1043
|
+
byId.set(String(r.record_id), rowToRecord(r));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/** 单条来源是否处于退场态:row 消失也算(与物理删传播同口径,宁保守勿复活)。 */
|
|
1047
|
+
const exited = (id) => {
|
|
1048
|
+
const rec = byId.get(id);
|
|
1049
|
+
if (!rec)
|
|
1050
|
+
return true;
|
|
1051
|
+
const state = classifyValidity({
|
|
1052
|
+
marker: readSupersedeMarker(rec.metadata),
|
|
1053
|
+
persistence: rec.persistence,
|
|
1054
|
+
validTo: rec.validTo,
|
|
1055
|
+
validToSource: validToSourceOf(rec.metadata),
|
|
1056
|
+
now,
|
|
1057
|
+
});
|
|
1058
|
+
return isExitedState(state);
|
|
1059
|
+
};
|
|
1060
|
+
const allExited = (ids) => ids.length > 0 && ids.every(exited);
|
|
1061
|
+
const allRevived = (ids) => ids.length > 0 && ids.every((id) => byId.has(id) && !exited(id));
|
|
1062
|
+
const toArchive = nodes.filter((n) => (n.status === 'active' || n.status === 'disputed') && allExited(n.sourceRecordIds));
|
|
1063
|
+
const toArchiveEdges = edges.filter((e) => (e.status === 'active' || e.status === 'disputed') && allExited(e.sourceRecordIds));
|
|
1064
|
+
const toRevive = nodes.filter((n) => n.status === 'archived' && allRevived(n.sourceRecordIds));
|
|
1065
|
+
const toReviveEdges = edges.filter((e) => e.status === 'archived' && allRevived(e.sourceRecordIds));
|
|
1066
|
+
if (toArchive.length === 0 && toArchiveEdges.length === 0 && toRevive.length === 0 && toReviveEdges.length === 0)
|
|
1067
|
+
return;
|
|
1068
|
+
const nowIso = new Date(now).toISOString();
|
|
1069
|
+
this.tx(() => {
|
|
1070
|
+
for (const n of toArchive) {
|
|
1071
|
+
this.db.prepare(`UPDATE graph_nodes SET status = 'archived', updated_time = ? WHERE node_id = ?`).run(nowIso, n.id);
|
|
1072
|
+
}
|
|
1073
|
+
for (const e of toArchiveEdges) {
|
|
1074
|
+
this.db.prepare(`UPDATE graph_edges SET status = 'archived', updated_time = ? WHERE edge_id = ?`).run(nowIso, e.id);
|
|
1075
|
+
}
|
|
1076
|
+
for (const n of toRevive) {
|
|
1077
|
+
this.db.prepare(`UPDATE graph_nodes SET status = 'active', updated_time = ? WHERE node_id = ?`).run(nowIso, n.id);
|
|
1078
|
+
}
|
|
1079
|
+
for (const e of toReviveEdges) {
|
|
1080
|
+
this.db.prepare(`UPDATE graph_edges SET status = 'active', updated_time = ? WHERE edge_id = ?`).run(nowIso, e.id);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
this.logger?.info(`${TAG} 退场传播(重算清扫):${toArchive.length + toArchiveEdges.length} 项标 archived,` +
|
|
1084
|
+
`${toRevive.length + toReviveEdges.length} 项复活(触发记录 ${changedIds.length} 条)`);
|
|
1085
|
+
}
|
|
1086
|
+
catch (err) {
|
|
1087
|
+
this.logger?.warn(`${TAG} 退场传播失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
877
1090
|
/**
|
|
878
1091
|
* §C 矛盾冻结:把图谱的 `disputed` 状态**同步**到给定冲突集。
|
|
879
1092
|
*
|
package/dist/store/l1.d.ts
CHANGED
|
@@ -252,6 +252,11 @@ export declare class L1Store {
|
|
|
252
252
|
};
|
|
253
253
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
254
254
|
getByIds(ids: string[]): MemoryRecord[];
|
|
255
|
+
/**
|
|
256
|
+
* 反向取代链(task_4.4,门3):返回"被 newId 取代"的退场记录 id。
|
|
257
|
+
* 走 MemoryDb 的 LIKE 全扫缝(百~千量级;阈值告警在那边)。
|
|
258
|
+
*/
|
|
259
|
+
findSupersededTargets(newId: string): string[];
|
|
255
260
|
/** 外部导入幂等 pre-check(轨3-P0):ext_key 命中已存在记录则返回其 id,否则 undefined。 */
|
|
256
261
|
findIdByExtKey(extKey: string): string | undefined;
|
|
257
262
|
/**
|
package/dist/store/l1.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, promises as fs, writeFileSync } from 'node:fs';
|
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { familyForType, isScopeVisible } from '../types.js';
|
|
12
12
|
import { isRetired } from './supersede.js';
|
|
13
|
+
import { stampValidity } from './validity.js';
|
|
13
14
|
import { exportThenPurge, readSnapshotManifest, readSnapshotRecords, restoreL1Snapshot, selectSnapshotTargets, snapshotDirFor, listSnapshots as listSnapshotsIn } from './l1-snapshot.js';
|
|
14
15
|
import { EmbedHelper, NoopEmbeddingService } from './embedding.js';
|
|
15
16
|
import { appendJsonl, dayKey, ensureDir, readJsonl } from '../util/io.js';
|
|
@@ -392,6 +393,13 @@ export class L1Store {
|
|
|
392
393
|
getByIds(ids) {
|
|
393
394
|
return this.db.getL1ByIds(ids);
|
|
394
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* 反向取代链(task_4.4,门3):返回"被 newId 取代"的退场记录 id。
|
|
398
|
+
* 走 MemoryDb 的 LIKE 全扫缝(百~千量级;阈值告警在那边)。
|
|
399
|
+
*/
|
|
400
|
+
findSupersededTargets(newId) {
|
|
401
|
+
return this.db.findSupersededTargets(newId);
|
|
402
|
+
}
|
|
395
403
|
/** 外部导入幂等 pre-check(轨3-P0):ext_key 命中已存在记录则返回其 id,否则 undefined。 */
|
|
396
404
|
findIdByExtKey(extKey) {
|
|
397
405
|
return this.db.findRecordIdByExtKey(extKey);
|
|
@@ -473,9 +481,14 @@ export class L1Store {
|
|
|
473
481
|
async appendNew(records) {
|
|
474
482
|
if (records.length === 0)
|
|
475
483
|
return;
|
|
484
|
+
// 写侧标注汇流口(task_3.2):所有新写路径(pipeline / memory_add / import / memport /
|
|
485
|
+
// 重建保留…)都经这里,统一盖 dsh_validity 快照;源头盖过的行(pipeline 带 evidence、
|
|
486
|
+
// 工具带 llm_inferred)保留其 validToSource,未盖过的补 'unknown'(fail-open)。
|
|
487
|
+
const stampNow = Date.now();
|
|
476
488
|
for (const r of records) {
|
|
477
489
|
if (!r.family)
|
|
478
490
|
r.family = familyForType(r.type);
|
|
491
|
+
stampValidity(r, stampNow);
|
|
479
492
|
}
|
|
480
493
|
// 起名必须在**追加事实源之前**:名字是文件身份键,进了 JSONL 才能跟记录一起重生。
|
|
481
494
|
// 这批还没进库,所以只算名字不落库(persist=false),由下面的 upsert 一并写。
|
|
@@ -506,7 +519,9 @@ export class L1Store {
|
|
|
506
519
|
async upsert(record) {
|
|
507
520
|
if (!record.family)
|
|
508
521
|
record.family = familyForType(record.type);
|
|
509
|
-
// merge 分支会继承胜方的 name
|
|
522
|
+
// 写侧标注汇流口(与 appendNew 同口径):merge 分支会继承胜方的 name;
|
|
523
|
+
// 两边都没名字时在这里补上,别让新铸的行继续带空名。
|
|
524
|
+
stampValidity(record, Date.now());
|
|
510
525
|
this.assignMissingNames([record], { persist: false });
|
|
511
526
|
const vec = (await this.helper.batch([record.content]))[0];
|
|
512
527
|
if (!this.db.upsertL1(record, vec)) {
|
|
@@ -584,8 +599,9 @@ export class L1Store {
|
|
|
584
599
|
// **只清理确实处于退场态的记录**。这道复核必须在删除发生的地方(而不是调用方):
|
|
585
600
|
// 否则任何调用方传一个 id 列表就能绕过软删、把活动记忆直接物理抹掉——
|
|
586
601
|
// 那等于给"先导出后清理"留了一条硬删后门。
|
|
602
|
+
// 门1 解耦:判据与全仓同为 marker 单源 `isRetired`(不再拿 valid_to 当退场凭据)。
|
|
587
603
|
const known = new Map(this.db.getL1ByIds(ids).map((r) => [r.id, r]));
|
|
588
|
-
const retiredIds = ids.filter((id) => known.get(id)
|
|
604
|
+
const retiredIds = ids.filter((id) => { const r = known.get(id); return r !== undefined && isRetired(r); });
|
|
589
605
|
return exportThenPurge(this.db, this.dataDir, retiredIds, reason, this.logger, new Date(), opts);
|
|
590
606
|
}
|
|
591
607
|
// ───────────────────────────────────────────────────────────────────────────
|