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/store/sqlite.js
CHANGED
|
@@ -30,17 +30,18 @@ const require = createRequire(import.meta.url);
|
|
|
30
30
|
const TAG = '[memory][sqlite]';
|
|
31
31
|
/**
|
|
32
32
|
* 退场判据的 **SQL 侧同口径表达**(与 `isRetired` / `upsertL1InTx` 里的 `retiredNow` 一一对应):
|
|
33
|
-
* `
|
|
33
|
+
* `metadata_json` 带退场标记即退场。门1 解耦(expiry-tiering R1)后**不再**看 `valid_to`——
|
|
34
|
+
* valid_to 回归纯现实有效期,"带合法历史 valid_to 的记录"是可检可注入的有效-历史态,不是退场。
|
|
34
35
|
*
|
|
35
36
|
* 为什么仓内只能有一份:退场记录有**三条检索面写路径绕过 upsert 漏斗**——
|
|
36
37
|
* FTS 重建回灌、增量重嵌入取数、补齐判据计数。任一处漏了就是:
|
|
37
38
|
* ① 退场行被放回检索面(前两处;重嵌入是 30 分钟一轮,比 FTS 重建更易命中);
|
|
38
39
|
* ② 补齐判据永不收敛(第三处:缺向量的退场行永远算“还差”,却永远不该被补)。
|
|
39
|
-
* 判据分头写,三处迟早各自漂移 ——
|
|
40
|
+
* 判据分头写,三处迟早各自漂移 —— 故集中成这一个函数(与 `supersede.isRetired` 是同口径一对)。
|
|
40
41
|
*/
|
|
41
42
|
function retiredL1Sql(alias) {
|
|
42
43
|
const p = alias ? `${alias}.` : '';
|
|
43
|
-
return
|
|
44
|
+
return `${p}metadata_json LIKE '%"${SUPERSEDE_METADATA_KEY}"%'`;
|
|
44
45
|
}
|
|
45
46
|
// 成本账本(token_cost 表族)独立职责,此处 re-export 不断裂既有引用;
|
|
46
47
|
// MemoryDb 的四个公开方法为一行委托。
|
|
@@ -669,6 +670,24 @@ export class MemoryDb {
|
|
|
669
670
|
catch {
|
|
670
671
|
/* 戳写失败只影响下次启动多一次重建,不阻断 */
|
|
671
672
|
}
|
|
673
|
+
// ── 门1 解耦一次性回灌(task_6.1):旧版拿 `valid_to<>''` 当退场判据,合法历史/
|
|
674
|
+
// 过期 valid_to 的记录被**静默遮蔽**(FTS/向量行都没建)。判据已改成 marker 单源,
|
|
675
|
+
// 但存量 FTS 不会自动复活那些行——升版后首启强制一次全量回灌(新判据下真退场的
|
|
676
|
+
// 行由 backfillL1Fts 自身的 retiredL1Sql 筛子跳过,日志报回灌/跳过数)。
|
|
677
|
+
// 幂等靠 stamp:跑过即不再重复(它是一次性修复,不是启动常项)。
|
|
678
|
+
if (this.readMetaString('gate1.retire-decoupled.v1') === null) {
|
|
679
|
+
try {
|
|
680
|
+
// 先清空再回灌:backfillL1Fts 的重灌语义假定目标表干净(它只在 DROP 重建后被调过),
|
|
681
|
+
// 直接在已有行的 FTS 上叠加会双插(同 id 命中两次)。
|
|
682
|
+
this.db.exec('DELETE FROM l1_fts');
|
|
683
|
+
this.backfillL1Fts();
|
|
684
|
+
this.writeMetaString('gate1.retire-decoupled.v1', new Date().toISOString());
|
|
685
|
+
this.logger?.info(`${TAG} 门1 解耦回灌完成:误遮蔽的历史记忆已回到检索面;向量面由增量重嵌自动补齐,或在设置页手动「语义检索重建」加速(不跑也只是补齐慢,不丢)`);
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
this.logger?.warn(`${TAG} 门1 解耦回灌失败(下次启动重试): ${err instanceof Error ? err.message : String(err)}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
672
691
|
this.ftsAvailable = true;
|
|
673
692
|
}
|
|
674
693
|
catch (err) {
|
|
@@ -995,11 +1014,13 @@ export class MemoryDb {
|
|
|
995
1014
|
// 全扫(批量写整体 O(N²))。只有主表已有该行(覆盖/合并)才可能有旧 FTS 行需要删。
|
|
996
1015
|
// 同批重复 id 也能正确处理:首条插入后,第二条的点查在同一事务内已见新行。
|
|
997
1016
|
const ftsExisted = this.ftsAvailable ? this.stmtL1Exists.get(record.id) !== undefined : false;
|
|
998
|
-
//
|
|
1017
|
+
// **退场不变量必须在写入漏斗上强制**:已退场(带取代标记)的记录
|
|
999
1018
|
// 永不出现在检索面。只靠 `retireL1Batch` 保证是不够的——快照恢复 / 重建 /
|
|
1000
1019
|
// 旧版导入都会经这里写回记录,若照常重建 FTS/向量行,一条带退场标记的记录
|
|
1001
|
-
// 会**悄悄回到检索结果里**(
|
|
1002
|
-
|
|
1020
|
+
// 会**悄悄回到检索结果里**(而检索 SQL 侧刻意不看 valid_to,正是为了零漂移)。
|
|
1021
|
+
// 门1 解耦:判据与 `isRetired`/`retiredL1Sql` 同为 marker 单源——合法历史 valid_to
|
|
1022
|
+
// 的行从这里正常进入检索面(旧口径的误遮蔽面由升版回灌收敛,task_6.1)。
|
|
1023
|
+
const retiredNow = readSupersedeMarker(record.metadata) !== undefined;
|
|
1003
1024
|
this.stmtUpsertL1.run(record.id, record.content, type, priority, sceneName, record.sessionId ?? 'default', record.version ?? 0, ts.str, ts.start, ts.end, toIso(record.createdAt), toIso(record.updatedAt), JSON.stringify(record.metadata ?? {}), family, toIso(record.validFrom), toIso(record.validTo), normPersistence(record.persistence) ?? '', scope, workspaceId, record.repoKeyName ?? '', record.repoKeyOwner ?? '', normApplicability(record.applicability), record.extKey ?? '',
|
|
1004
1025
|
// 文件身份键。存什么由调用方定(`deriveName` 已按 `sanitizeFilename` 纪律截断/洗字符),
|
|
1005
1026
|
// 这里只做 undefined → '' 的绑定兜底,与同一行其他可选列同口径。
|
|
@@ -1052,27 +1073,29 @@ export class MemoryDb {
|
|
|
1052
1073
|
* **软删**(记忆退场):保留主表行,撤出检索面。
|
|
1053
1074
|
*
|
|
1054
1075
|
* 与 `deleteL1Batch` 的差别**只有一处**:不动 `l1_records` 行本身。
|
|
1055
|
-
* `
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1076
|
+
* `metadata_json` 写取代标记 → 记录仍能被 `listL1` 列出、能被 `clearRetireMarker`
|
|
1077
|
+
* + upsert 恢复;而 FTS 与向量行照旧删除,于是检索面(含去重候选召回)自然看不到它。
|
|
1078
|
+
*
|
|
1079
|
+
* 门1 解耦(expiry-tiering R1/task_2.3):退场**只写 marker,不再闭合 valid_to**——
|
|
1080
|
+
* valid_to 回归纯现实有效期,退场与否由 marker 单源回答(`isRetired`)。
|
|
1059
1081
|
*
|
|
1060
1082
|
* 顺序刻意如此:先打标记(可逆的那一半),再撤检索面,且整体在一个事务里。
|
|
1061
1083
|
* 反过来先撤索引而打标记失败,记录会落在"检索不到、也没被标记"的状态 ——
|
|
1062
1084
|
* 既查不出来也恢复不了,是最坏的一种中间态。
|
|
1063
1085
|
*
|
|
1064
|
-
* **幂等**:已退场(
|
|
1086
|
+
* **幂等**:已退场(带 marker)的 id 不再重复写标记,
|
|
1065
1087
|
* 保留首次退场的原因与时刻(「谁先取代了它」不该被后一次调用改写)。
|
|
1066
1088
|
*
|
|
1067
|
-
* **不调** `graphStore.markSourcesDeleted
|
|
1068
|
-
*
|
|
1089
|
+
* **不调** `graphStore.markSourcesDeleted`(那是"来源已物理消失"的传播,软删行一直在),
|
|
1090
|
+
* 而是调 `markSourcesRetired`(计划 R9 对偶刀):按"来源是否全部退场态"重算清扫,
|
|
1091
|
+
* 全退场的 active/disputed 节点·边标 archived 墓碑(派生投影由当前事实重算,非事件改写)。
|
|
1069
1092
|
*/
|
|
1070
1093
|
retireL1Batch(ids, info) {
|
|
1071
1094
|
if (this.degraded || ids.length === 0)
|
|
1072
1095
|
return 0;
|
|
1073
1096
|
try {
|
|
1074
1097
|
const existing = new Map(this.getL1ByIds(ids).map((r) => [r.id, r]));
|
|
1075
|
-
const update = this.db.prepare('UPDATE l1_records SET
|
|
1098
|
+
const update = this.db.prepare('UPDATE l1_records SET metadata_json = ? WHERE record_id = ?');
|
|
1076
1099
|
const retired = [];
|
|
1077
1100
|
this.withTransaction(() => {
|
|
1078
1101
|
for (const id of ids) {
|
|
@@ -1080,12 +1103,16 @@ export class MemoryDb {
|
|
|
1080
1103
|
// 不存在的 id 静默跳过;已退场的跳过以保幂等
|
|
1081
1104
|
if (!rec || isRetired(rec))
|
|
1082
1105
|
continue;
|
|
1083
|
-
update.run(
|
|
1106
|
+
update.run(JSON.stringify(withSupersedeMarker(rec.metadata, info)), id);
|
|
1084
1107
|
retired.push(id);
|
|
1085
1108
|
}
|
|
1086
1109
|
if (retired.length > 0)
|
|
1087
1110
|
this.detachL1FromRetrieval(retired);
|
|
1088
1111
|
});
|
|
1112
|
+
// 图谱退场传播在事务外(与 deleteL1Batch 同形态):清扫失败只 warn,
|
|
1113
|
+
// 不回滚已经成立的软删——库侧结果先成立,图谱是旁路投影。
|
|
1114
|
+
if (retired.length > 0)
|
|
1115
|
+
this.graphStore.markSourcesRetired(retired);
|
|
1089
1116
|
return retired.length;
|
|
1090
1117
|
}
|
|
1091
1118
|
catch (err) {
|
|
@@ -1111,25 +1138,32 @@ export class MemoryDb {
|
|
|
1111
1138
|
* 清掉退场标记(恢复的**前半**)。返回清完标记的记录,供调用方 re-upsert 以重建
|
|
1112
1139
|
* FTS/向量 —— 那条路径(`upsertL1InTx`)已存在,不在这里重复实现。
|
|
1113
1140
|
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1141
|
+
* 门1 解耦(task_2.3 的对偶面):只剥 marker,**不碰 valid_to**——
|
|
1142
|
+
* 行若本就带着真实历史有效期(导入/抽取写入),恢复不该把它抹掉;
|
|
1143
|
+
* 旧版 retire 写进 valid_to 的退场时刻对新行只影响 classify 落到
|
|
1144
|
+
* historical/expired_suspect(仍可检可注入,带标注),由 validity-backfill 逐步纠偏。
|
|
1145
|
+
* 返回的 `validTo` 保持原值——upsert 照原样写回,不会静默改列。
|
|
1117
1146
|
*/
|
|
1118
1147
|
clearRetireMarker(ids) {
|
|
1119
1148
|
if (this.degraded || ids.length === 0)
|
|
1120
1149
|
return [];
|
|
1121
1150
|
try {
|
|
1122
1151
|
const existing = this.getL1ByIds(ids);
|
|
1123
|
-
const update = this.db.prepare('UPDATE l1_records SET
|
|
1152
|
+
const update = this.db.prepare('UPDATE l1_records SET metadata_json = ? WHERE record_id = ?');
|
|
1124
1153
|
const restored = [];
|
|
1125
1154
|
this.withTransaction(() => {
|
|
1126
1155
|
for (const rec of existing) {
|
|
1127
1156
|
const metadata = stripSupersedeMarker(rec.metadata);
|
|
1128
1157
|
if (isRetired(rec))
|
|
1129
|
-
update.run(
|
|
1130
|
-
restored.push({ ...rec, metadata
|
|
1158
|
+
update.run(JSON.stringify(metadata), rec.id);
|
|
1159
|
+
restored.push({ ...rec, metadata });
|
|
1131
1160
|
}
|
|
1132
1161
|
});
|
|
1162
|
+
// 复活回药(R9 对偶刀的读侧):退场标记已清 → 同源重算清扫会把"来源全部复活"
|
|
1163
|
+
// 的 archived 节点/边拉回 active(与 retireL1Batch 的传播共用 sweep,方向由当前事实定)。
|
|
1164
|
+
if (restored.length > 0) {
|
|
1165
|
+
this.graphStore.markSourcesRetired(restored.map((r) => r.id));
|
|
1166
|
+
}
|
|
1133
1167
|
return restored;
|
|
1134
1168
|
}
|
|
1135
1169
|
catch (err) {
|
|
@@ -1137,17 +1171,17 @@ export class MemoryDb {
|
|
|
1137
1171
|
return [];
|
|
1138
1172
|
}
|
|
1139
1173
|
}
|
|
1140
|
-
/** 已退场记录列表(面板用)
|
|
1174
|
+
/** 已退场记录列表(面板用):与 `retiredL1Sql()` 同判据(marker 单源)。失败返回空。 */
|
|
1141
1175
|
listRetiredL1(opts) {
|
|
1142
1176
|
if (this.degraded)
|
|
1143
1177
|
return { items: [], total: 0 };
|
|
1144
1178
|
try {
|
|
1145
1179
|
const totalRow = this.db
|
|
1146
|
-
.prepare(
|
|
1180
|
+
.prepare(`SELECT COUNT(*) AS n FROM l1_records WHERE ${retiredL1Sql()}`)
|
|
1147
1181
|
.get();
|
|
1148
1182
|
const rows = this.db
|
|
1149
1183
|
.prepare(`SELECT record_id, content, type, priority, scene_name, version, timestamp_str, created_time, updated_time, metadata_json, family, valid_from, valid_to, persistence, scope, workspace_id, repo_key_name, repo_key_owner, applicability, name
|
|
1150
|
-
FROM l1_records WHERE
|
|
1184
|
+
FROM l1_records WHERE ${retiredL1Sql()} ORDER BY updated_time DESC LIMIT ? OFFSET ?`)
|
|
1151
1185
|
.all(opts.limit, opts.offset);
|
|
1152
1186
|
return { items: rows.map(rowToRecord), total: Number(totalRow?.n ?? 0) };
|
|
1153
1187
|
}
|
|
@@ -1175,11 +1209,15 @@ export class MemoryDb {
|
|
|
1175
1209
|
// 文件身份键(md 投射相):同款形状探测。没这一列的旧库读侧归一 undefined,
|
|
1176
1210
|
// 不报错——投射器把空 name 回落到 `mem-<id8>`,而不是把整条取记录路径弄挂。
|
|
1177
1211
|
const nameCol = table === 'l1_records' && this.hasColumn('l1_records', 'name') ? ', name' : '';
|
|
1212
|
+
// 外部导入幂等键(轨3-P0):同款形状探测。此前读缝只写不回读——"血缘字段随
|
|
1213
|
+
// memory_update 带走"需要它;缺列旧库读侧归一 undefined(fail-open)。
|
|
1214
|
+
const extCol = table === 'l1_records' && this.hasColumn('l1_records', 'ext_key') ? ', ext_key' : '';
|
|
1178
1215
|
const metaCols = 'record_id, content, type, priority, scene_name, version, timestamp_str, created_time, updated_time, metadata_json, family' +
|
|
1179
1216
|
temporal +
|
|
1180
1217
|
scopeCols +
|
|
1181
1218
|
repoCols +
|
|
1182
|
-
nameCol
|
|
1219
|
+
nameCol +
|
|
1220
|
+
extCol;
|
|
1183
1221
|
stmt =
|
|
1184
1222
|
action === 'delete'
|
|
1185
1223
|
? this.db.prepare(`DELETE FROM ${table} WHERE record_id IN (${ph})`)
|
|
@@ -1920,13 +1958,13 @@ export class MemoryDb {
|
|
|
1920
1958
|
// json_valid 守卫口径;与 l1RoomCounts() 计数互补(rooms 计数 + 孤儿数 = 全量)。
|
|
1921
1959
|
where.push(`NOT EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{}' END, '$.tags'))`);
|
|
1922
1960
|
}
|
|
1923
|
-
// 退场筛查(三态,见 opts.retired 注释):与 listRetiredL1
|
|
1924
|
-
//
|
|
1961
|
+
// 退场筛查(三态,见 opts.retired 注释):与 listRetiredL1 同一 `retiredL1Sql` 判据,
|
|
1962
|
+
// 保证「仅退场」视图与「已退场」区看到同一批行(门1 解耦后同为 marker 单源)。
|
|
1925
1963
|
if (opts.retired === false) {
|
|
1926
|
-
where.push(
|
|
1964
|
+
where.push(`NOT (${retiredL1Sql()})`);
|
|
1927
1965
|
}
|
|
1928
1966
|
else if (opts.retired === true) {
|
|
1929
|
-
where.push(
|
|
1967
|
+
where.push(retiredL1Sql());
|
|
1930
1968
|
}
|
|
1931
1969
|
const whereSql = where.length > 0 ? ` WHERE ${where.join(' AND ')}` : '';
|
|
1932
1970
|
const totalRow = this.db.prepare(`SELECT COUNT(*) AS n FROM l1_records${whereSql}`).get(...params);
|
|
@@ -1988,13 +2026,38 @@ export class MemoryDb {
|
|
|
1988
2026
|
return 0;
|
|
1989
2027
|
}
|
|
1990
2028
|
}
|
|
1991
|
-
/**
|
|
2029
|
+
/**
|
|
2030
|
+
* 反向取代链查询(task_4.4,门3):"谁被这条记录取代了"——扫 metadata_json 里
|
|
2031
|
+
* 取代标记的 `by` 指向本 id 的行。百~千量级全扫可接受(与 `retiredL1Sql` 同属
|
|
2032
|
+
* LIKE 面);超过阈值时留 warn——真到万级该建索引,不在本计划面里预先造。
|
|
2033
|
+
* **只回退场行**:"被取代者"正是带 marker 的旧行(活跃行若 by 指过来是脏数据);
|
|
2034
|
+
* 模式含闭合引号(`"by":"<id>"`),不会前缀误配更长 id。
|
|
2035
|
+
*/
|
|
2036
|
+
findSupersededTargets(newId) {
|
|
2037
|
+
if (this.degraded || !newId)
|
|
2038
|
+
return [];
|
|
2039
|
+
const like = `%"by":${JSON.stringify(newId)}%`;
|
|
2040
|
+
try {
|
|
2041
|
+
const rows = this.db
|
|
2042
|
+
.prepare(`SELECT record_id FROM l1_records WHERE metadata_json LIKE ? AND ${retiredL1Sql()}`)
|
|
2043
|
+
.all(like);
|
|
2044
|
+
if (rows.length > 2000) {
|
|
2045
|
+
this.logger?.warn(`${TAG} 反向取代链命中 ${rows.length} 条:超过全扫舒适区,考虑为 dsh_superseded.by 建索引`);
|
|
2046
|
+
}
|
|
2047
|
+
return rows.map((r) => r.record_id);
|
|
2048
|
+
}
|
|
2049
|
+
catch (err) {
|
|
2050
|
+
this.logger?.warn(`${TAG} 反向取代链查询失败(返回空): ${err instanceof Error ? err.message : String(err)}`);
|
|
2051
|
+
return [];
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
/** retired 行计数(退场判定与 listRetiredL1 同口径:`retiredL1Sql` marker 单源)。失败返回 0。 */
|
|
1992
2055
|
retiredL1Count() {
|
|
1993
2056
|
if (this.degraded)
|
|
1994
2057
|
return 0;
|
|
1995
2058
|
try {
|
|
1996
2059
|
const row = this.db
|
|
1997
|
-
.prepare(
|
|
2060
|
+
.prepare(`SELECT COUNT(*) AS n FROM l1_records WHERE ${retiredL1Sql()}`)
|
|
1998
2061
|
.get();
|
|
1999
2062
|
return Number(row?.n ?? 0);
|
|
2000
2063
|
}
|
|
@@ -2733,6 +2796,9 @@ function rowToRecord(row) {
|
|
|
2733
2796
|
// 文件身份键回读:空串归一 undefined(“还没名字”与“名字是空串”不是一种意思),
|
|
2734
2797
|
// 缺列的旧库也是 undefined。读侧同样 fail-open。
|
|
2735
2798
|
name: row.name || undefined,
|
|
2799
|
+
// 外部导入幂等键回读:空串 = 内部记录(不参与幂等),归一 undefined——
|
|
2800
|
+
// memory_update 靠它把导入身份带进新版本,丢了它同来源重导会静默重复累积。
|
|
2801
|
+
extKey: row.ext_key || undefined,
|
|
2736
2802
|
};
|
|
2737
2803
|
}
|
|
2738
2804
|
/**
|
|
@@ -74,5 +74,13 @@ export declare function readSupersedeMarker(metadata: unknown): SupersedeInfo |
|
|
|
74
74
|
* 这条判据让"恢复"对活动记录是彻底的 no-op。
|
|
75
75
|
*/
|
|
76
76
|
export declare function stripSupersedeMarker(metadata: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
77
|
-
/**
|
|
78
|
-
|
|
77
|
+
/**
|
|
78
|
+
* 该记录是否处于**已退场**状态(退场判据的唯一读侧封装)。
|
|
79
|
+
*
|
|
80
|
+
* 门1 解耦(expiry-tiering R1):只认 marker,**不再**把 `validTo !== undefined` 当退场——
|
|
81
|
+
* valid_to 回归纯现实有效期(一条 2020 年结束的旅行记忆没有退场,只是成了历史)。
|
|
82
|
+
* 旧口径把"过时""历史区间""软删"三件事塞进一根列,导致合法带 valid_to 的存量记录
|
|
83
|
+
* 被检索面静默遮蔽——那是本计划要根治的病根(误遮蔽历史由升版回灌收敛,见迁移 task_6.1)。
|
|
84
|
+
* `retiredL1Sql()`(sqlite.ts)与本函数是全仓同口径的一对,改动必须两处同步。
|
|
85
|
+
*/
|
|
86
|
+
export declare function isRetired(record: Pick<MemoryRecord, 'metadata'>): boolean;
|
package/dist/store/supersede.js
CHANGED
|
@@ -76,7 +76,15 @@ export function stripSupersedeMarker(metadata) {
|
|
|
76
76
|
delete next[SUPERSEDE_METADATA_KEY];
|
|
77
77
|
return next;
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* 该记录是否处于**已退场**状态(退场判据的唯一读侧封装)。
|
|
81
|
+
*
|
|
82
|
+
* 门1 解耦(expiry-tiering R1):只认 marker,**不再**把 `validTo !== undefined` 当退场——
|
|
83
|
+
* valid_to 回归纯现实有效期(一条 2020 年结束的旅行记忆没有退场,只是成了历史)。
|
|
84
|
+
* 旧口径把"过时""历史区间""软删"三件事塞进一根列,导致合法带 valid_to 的存量记录
|
|
85
|
+
* 被检索面静默遮蔽——那是本计划要根治的病根(误遮蔽历史由升版回灌收敛,见迁移 task_6.1)。
|
|
86
|
+
* `retiredL1Sql()`(sqlite.ts)与本函数是全仓同口径的一对,改动必须两处同步。
|
|
87
|
+
*/
|
|
80
88
|
export function isRetired(record) {
|
|
81
|
-
return
|
|
89
|
+
return readSupersedeMarker(record.metadata) !== undefined;
|
|
82
90
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆**有效性定性**的标注纯函数层(计划 expiry-tiering §2.1 冻结契约 · 先行刀)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `supersede.ts` 同构:读写成对、可被单测直接验,不起库不起宿主。
|
|
5
|
+
* 本层承载三档分级 + 两类取代态的**判据单源**——`classifyValidity` 是全仓
|
|
6
|
+
* 唯一有权回答"这条记录现在算什么态"的函数;检索/注入/图谱退役传播都只
|
|
7
|
+
* 消费它的结论,不得再各写一份 valid_to/marker 比较。
|
|
8
|
+
*
|
|
9
|
+
* ## 两条铁律(与 supersede.ts 相同)
|
|
10
|
+
*
|
|
11
|
+
* 1. **不可变**:`with*` / `strip*` 一律返回**新对象**,绝不改入参。
|
|
12
|
+
* 2. **零漂移**:无标注时 `strip` 返回**原引用**;`with` 只在确有 state 时才写键
|
|
13
|
+
* ——既有记录的 `metadata_json` 不该因为本功能上线而多出任何字节。
|
|
14
|
+
*
|
|
15
|
+
* 保留键带 `dsh_` 前缀的理由与 `SUPERSEDE_METADATA_KEY` 完全相同:写库时保留键
|
|
16
|
+
* 会与 LLM 产出的 metadata 合并进同一个 JSON 对象,必须靠命名空间隔开,
|
|
17
|
+
* 否则一次 LLM 幻觉输出 `expired` 就能伪装成系统定性。
|
|
18
|
+
*/
|
|
19
|
+
import type { MemoryRecord, Persistence } from '../types.js';
|
|
20
|
+
import { type SupersedeInfo } from './supersede.js';
|
|
21
|
+
/** `l1_records.metadata_json` 里承载有效性标注的**保留键**。 */
|
|
22
|
+
export declare const VALIDITY_METADATA_KEY = "dsh_validity";
|
|
23
|
+
/**
|
|
24
|
+
* 有效性六态(spec §0 分级模型):
|
|
25
|
+
* - `current` 有效-当前(仍在持续且未到有效期)
|
|
26
|
+
* - `historical` 有效-历史(时点/已结束区间/无时间性,未退场)
|
|
27
|
+
* - `expired` 过时(仍在持续但 valid_to≤now,且 valid_to 来源可信)
|
|
28
|
+
* - `expired_suspect` 假过时嫌疑(同上但 valid_to 来源不可信,不硬隐藏)
|
|
29
|
+
* - `superseded` 被取代(marker reason='superseded'/'conflict',撤出检索面)
|
|
30
|
+
* - `retired` 人工/废弃退场(marker reason='manual'/'obsolete')
|
|
31
|
+
*/
|
|
32
|
+
export type ValidityState = 'current' | 'historical' | 'expired' | 'expired_suspect' | 'superseded' | 'retired';
|
|
33
|
+
/** valid_to 的证据来源:`evidence` 之外一律视为不可信(判不出"真过时")。 */
|
|
34
|
+
export type ValidToSource = 'evidence' | 'llm_inferred' | 'fallback_now' | 'unknown';
|
|
35
|
+
/** 写进 metadata 的标注负载。`state` 必填,其余缺省即不写键。 */
|
|
36
|
+
export interface ValidityInfo {
|
|
37
|
+
state: ValidityState;
|
|
38
|
+
validToSource: ValidToSource;
|
|
39
|
+
/** 判定依据的一句话说明(面板/审计可读,不做判据)。 */
|
|
40
|
+
basis: string;
|
|
41
|
+
/** 标注时刻(ISO 8601)。 */
|
|
42
|
+
at: string;
|
|
43
|
+
/** 取代它的记录 id(透传 supersede 标记,仅 state='superseded' 场景有值)。 */
|
|
44
|
+
by?: string;
|
|
45
|
+
/** 冲突对 id(透传,仅退场两类有值)。 */
|
|
46
|
+
pairId?: string;
|
|
47
|
+
/** 判定置信度 0..1(证据强度注记,不参与判据)。 */
|
|
48
|
+
confidence?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 写入有效性标注(返回**新对象**)。
|
|
52
|
+
*
|
|
53
|
+
* `info` 缺失或没有合法 `state` 时**不写键并原样返回**——"未判定"不是一种标注,
|
|
54
|
+
* 不该产生一个空壳键占位(零漂移纪律,对齐 task_1.7 的字节断言)。
|
|
55
|
+
*/
|
|
56
|
+
export declare function withValidity(metadata: Record<string, unknown> | undefined, info: (Partial<ValidityInfo> & {
|
|
57
|
+
state?: ValidityState;
|
|
58
|
+
}) | undefined): Record<string, unknown>;
|
|
59
|
+
/**
|
|
60
|
+
* 读回有效性标注(读侧**唯一入口**,白名单投影)。
|
|
61
|
+
*
|
|
62
|
+
* 形状校验从严:`state` 缺失或不在六态枚举内 → 返回 `undefined`(当作**未标注**)。
|
|
63
|
+
* 未标注的记录由调用方按 fail-open 处理,绝不让一个坏键把记忆判成"过时"而藏起来。
|
|
64
|
+
*/
|
|
65
|
+
export declare function readValidity(metadata: unknown): ValidityInfo | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* 去标注(重判前清旧键用)。返回**新对象**;无键时返回**原引用**——
|
|
68
|
+
* 对未标注记录做 strip 是彻底的 no-op,不产生任何新对象。
|
|
69
|
+
*/
|
|
70
|
+
export declare function stripValidity(metadata: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
71
|
+
/** classifyValidity 的入参(全部来自记录现值,now 由调用方注入以便测试)。 */
|
|
72
|
+
export interface ClassifyValidityInput {
|
|
73
|
+
/** 退场标记(readSupersedeMarker 的结果);存在即优先于时间轴判定。 */
|
|
74
|
+
marker?: SupersedeInfo;
|
|
75
|
+
/** 客观持续性;缺省 = 未判定(fail-open)。 */
|
|
76
|
+
persistence?: Persistence;
|
|
77
|
+
/** 现实有效期(epoch ms);缺省 = 未闭合。 */
|
|
78
|
+
validTo?: number;
|
|
79
|
+
/** valid_to 的证据来源;缺省按 'unknown'(不可信,判不出真过时)。 */
|
|
80
|
+
validToSource?: ValidToSource;
|
|
81
|
+
/** 判定时刻(epoch ms)。 */
|
|
82
|
+
now: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 六态判据(spec §0 表,唯一真源)。
|
|
86
|
+
*
|
|
87
|
+
* 判定次序:退场标记 > 时间轴。marker 存在即 `superseded`/`retired`,
|
|
88
|
+
* 不看 valid_to——"软删遮蔽"与"过时"是正交两轴,marker 是既成事实。
|
|
89
|
+
* persistence 未判定时 **fail-open**:valid_to 未闭合 → `current`;
|
|
90
|
+
* 已闭合(无论来源)→ `expired_suspect`(宁可标注疑,也不静默隐藏)。
|
|
91
|
+
*/
|
|
92
|
+
export declare function classifyValidity(input: ClassifyValidityInput): ValidityState;
|
|
93
|
+
/** 六态里是否属于**退场态**(撤出检索面的那两档)。 */
|
|
94
|
+
export declare function isExitedState(state: ValidityState): boolean;
|
|
95
|
+
/** 从记录 metadata 推 validToSource(未标注一律 'unknown',判不出可信过时)。 */
|
|
96
|
+
export declare function validToSourceOf(metadata: unknown): ValidToSource;
|
|
97
|
+
/** 门3 反查参数(工具与 RPC 共用;全缺省 = 默认行为)。 */
|
|
98
|
+
export interface ValidityGateOptions {
|
|
99
|
+
/** 放行当前 `expired`(可信过时)行;缺省 false。 */
|
|
100
|
+
includeExpired?: boolean;
|
|
101
|
+
/** valid 轴回放时刻(epoch ms):按 `validFrom ≤ T < (validTo ?? ∞)` 判可见,
|
|
102
|
+
* 只放宽 valid 轴、**不复活退场**(marker 行一律排除)。与 includeExpired 同给时 asOf 优先。 */
|
|
103
|
+
asOf?: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 检索出口的时效可见性谓词(门3,工具与面板 RPC 共用一份——防两处漂移)。
|
|
107
|
+
*
|
|
108
|
+
* 存在理由:门1 解除索引遮蔽后,带合法历史/过期 valid_to 的行会进检索命中;
|
|
109
|
+
* "默认只给有效"由出口层这刀维持(与改前"索引天然遮蔽 expired"的输出逐字等价)。
|
|
110
|
+
* 退场两档(superseded/retired)已被门1 索引层挡在命中之外,这里再查一道
|
|
111
|
+
* 只为 `asOf` 回放路径("不复活退场"的显式兑现),不是重复过滤。
|
|
112
|
+
*/
|
|
113
|
+
export declare function passesValidityGate(record: Pick<MemoryRecord, 'metadata' | 'validTo' | 'validFrom' | 'persistence' | 'createdAt'>, now: number, opts?: ValidityGateOptions): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* 记录现态的**读侧**定性(expiry-tiering 门2/门3 与图谱传播共用):
|
|
116
|
+
* 六态由记录现值 + 当下墙钟**查询期谓词**算出,不吃落库快照——
|
|
117
|
+
* `expired` 随时间自然成立,拿写入时缓存的 state 会把"当时还没过时"当成永真。
|
|
118
|
+
* 标注(`dsh_validity`)只为面板/审计留一份"写当时怎么判的",不是判据本身。
|
|
119
|
+
*/
|
|
120
|
+
export declare function stateOfRecord(record: Pick<MemoryRecord, 'metadata' | 'validTo' | 'persistence'>, now: number): ValidityState;
|
|
121
|
+
/** 门2 注入过滤的产出(task_3.3/3.4 可单测形状)。 */
|
|
122
|
+
export interface InjectFilterResult<T> {
|
|
123
|
+
/** 剔除可信 `expired` 后的候选(保序)。 */
|
|
124
|
+
kept: T[];
|
|
125
|
+
/** `expired_suspect` 的 id 集(注入行带"疑似失效"标注,不硬隐藏)。 */
|
|
126
|
+
suspectIds: Set<string>;
|
|
127
|
+
/** 因退场态被挡的条数(superseded/retired 本由门1 索引层排除,这里只记现算命中的防御数)。 */
|
|
128
|
+
exited: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 被动注入候选的时效过滤(门2 主体,纯函数供单测;调用点在 `hooks/recall.ts`,
|
|
132
|
+
* 仅 `tieringEnabled` 为真时进入——关时逐字现状由调用点门保证)。
|
|
133
|
+
*
|
|
134
|
+
* 判据单源 `classifyValidity`(经 stateOfRecord 现算),不另写 valid_to/marker 比较(计划 §4 约束)。
|
|
135
|
+
* 行缺失 fail-open 保留:宁多注一条也不静默丢记忆。
|
|
136
|
+
*/
|
|
137
|
+
export declare function filterInjectCandidates<T extends {
|
|
138
|
+
id: string;
|
|
139
|
+
}>(candidates: readonly T[], recordOf: (id: string) => MemoryRecord | undefined, now: number): InjectFilterResult<T>;
|
|
140
|
+
/**
|
|
141
|
+
* 写侧标注钩子(expiry-tiering 门2/门3 的写入时机,spec §2.1"派生时机"):
|
|
142
|
+
* 对**新建/重写**的记录就地盖一份 `dsh_validity` 快照(供面板/审计/反查展示,
|
|
143
|
+
* 判定仍以 `stateOfRecord` 现算为准——本快照不是判据)。
|
|
144
|
+
*
|
|
145
|
+
* 直接变异传入 record(与 `familyForType` 兜底同位:写侧拥有刚建的记录对象;
|
|
146
|
+
* 检索命中等共享对象绝不得经本函数改写)。`sourceHint` 只在尚无标注时生效:
|
|
147
|
+
* 管线/工具在源头就知道 valid_to 的证据链,盖得越早越准;汇流口补盖拿不到
|
|
148
|
+
* 源头信息时留 'unknown'(fail-open,只能判疑不能判死)。
|
|
149
|
+
*/
|
|
150
|
+
export declare function stampValidity(record: MemoryRecord, now: number, sourceHint?: ValidToSource): void;
|