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.
Files changed (64) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +62 -1
  3. package/README.md +60 -2
  4. package/dist/client.js +45 -5
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +33 -3
  7. package/dist/contract.d.ts +14 -1
  8. package/dist/embedding-child.cjs +192 -0
  9. package/dist/embedding-worker.cjs +2 -0
  10. package/dist/graph/apply.d.ts +9 -0
  11. package/dist/graph/apply.js +12 -2
  12. package/dist/graph/search.d.ts +11 -2
  13. package/dist/graph/search.js +38 -10
  14. package/dist/hooks/recall.js +28 -1
  15. package/dist/index.js +14 -1
  16. package/dist/llm.d.ts +21 -2
  17. package/dist/llm.js +56 -25
  18. package/dist/pipeline/graph.d.ts +17 -1
  19. package/dist/pipeline/graph.js +45 -2
  20. package/dist/pipeline/l1.d.ts +15 -2
  21. package/dist/pipeline/l1.js +161 -47
  22. package/dist/pipeline/l2.js +72 -50
  23. package/dist/pipeline/md-sync.js +4 -2
  24. package/dist/pipeline/ruminate.js +2 -0
  25. package/dist/pipeline/runner.d.ts +32 -0
  26. package/dist/pipeline/runner.js +109 -11
  27. package/dist/pipeline/slot-migrate.d.ts +3 -2
  28. package/dist/pipeline/slot-migrate.js +3 -2
  29. package/dist/projection/recall.d.ts +10 -3
  30. package/dist/projection/recall.js +14 -0
  31. package/dist/settings.js +6 -0
  32. package/dist/stats.d.ts +1 -1
  33. package/dist/stats.js +35 -14
  34. package/dist/store/embedding-source.d.ts +4 -2
  35. package/dist/store/embedding-source.js +5 -2
  36. package/dist/store/file-versions.d.ts +2 -0
  37. package/dist/store/file-versions.js +2 -0
  38. package/dist/store/graph-store.d.ts +71 -1
  39. package/dist/store/graph-store.js +221 -8
  40. package/dist/store/l1.d.ts +5 -0
  41. package/dist/store/l1.js +18 -2
  42. package/dist/store/local-embedding.d.ts +9 -5
  43. package/dist/store/local-embedding.js +137 -17
  44. package/dist/store/memory-md-disable.d.ts +3 -2
  45. package/dist/store/memory-md.d.ts +3 -3
  46. package/dist/store/memory-md.js +6 -4
  47. package/dist/store/route-health.d.ts +38 -0
  48. package/dist/store/route-health.js +115 -0
  49. package/dist/store/runtime-installer.d.ts +13 -1
  50. package/dist/store/runtime-installer.js +40 -2
  51. package/dist/store/sqlite.d.ts +24 -12
  52. package/dist/store/sqlite.js +97 -31
  53. package/dist/store/supersede.d.ts +10 -2
  54. package/dist/store/supersede.js +10 -2
  55. package/dist/store/validity.d.ts +150 -0
  56. package/dist/store/validity.js +209 -0
  57. package/dist/tools/index.d.ts +15 -1
  58. package/dist/tools/index.js +393 -11
  59. package/dist/util/json-repair.d.ts +48 -0
  60. package/dist/util/json-repair.js +285 -0
  61. package/dist/validity-backfill.d.ts +78 -0
  62. package/dist/validity-backfill.js +160 -0
  63. package/dsh.plugin.json +2 -2
  64. package/package.json +3 -3
@@ -0,0 +1,209 @@
1
+ import { readSupersedeMarker } from './supersede.js';
2
+ /** `l1_records.metadata_json` 里承载有效性标注的**保留键**。 */
3
+ export const VALIDITY_METADATA_KEY = 'dsh_validity';
4
+ const VALIDITY_STATES = [
5
+ 'current', 'historical', 'expired', 'expired_suspect', 'superseded', 'retired',
6
+ ];
7
+ const VALID_TO_SOURCES = ['evidence', 'llm_inferred', 'fallback_now', 'unknown'];
8
+ /**
9
+ * 写入有效性标注(返回**新对象**)。
10
+ *
11
+ * `info` 缺失或没有合法 `state` 时**不写键并原样返回**——"未判定"不是一种标注,
12
+ * 不该产生一个空壳键占位(零漂移纪律,对齐 task_1.7 的字节断言)。
13
+ */
14
+ export function withValidity(metadata, info) {
15
+ const base = metadata ?? {};
16
+ if (!info || typeof info.state !== 'string' || !VALIDITY_STATES.includes(info.state))
17
+ return base;
18
+ const marker = {
19
+ state: info.state,
20
+ validToSource: VALID_TO_SOURCES.includes(info.validToSource ?? 'unknown') ? (info.validToSource ?? 'unknown') : 'unknown',
21
+ basis: typeof info.basis === 'string' ? info.basis : '',
22
+ at: typeof info.at === 'string' ? info.at : '',
23
+ };
24
+ // 可选字段"有值才写键"(与 supersede.ts 同一语义:空串无法与"没有该字段"区分)
25
+ if (info.by)
26
+ marker.by = info.by;
27
+ if (info.pairId)
28
+ marker.pairId = info.pairId;
29
+ if (typeof info.confidence === 'number' && Number.isFinite(info.confidence)) {
30
+ marker.confidence = Math.max(0, Math.min(1, info.confidence));
31
+ }
32
+ return { ...base, [VALIDITY_METADATA_KEY]: marker };
33
+ }
34
+ /**
35
+ * 读回有效性标注(读侧**唯一入口**,白名单投影)。
36
+ *
37
+ * 形状校验从严:`state` 缺失或不在六态枚举内 → 返回 `undefined`(当作**未标注**)。
38
+ * 未标注的记录由调用方按 fail-open 处理,绝不让一个坏键把记忆判成"过时"而藏起来。
39
+ */
40
+ export function readValidity(metadata) {
41
+ if (metadata === null || typeof metadata !== 'object')
42
+ return undefined;
43
+ const raw = metadata[VALIDITY_METADATA_KEY];
44
+ if (raw === null || typeof raw !== 'object')
45
+ return undefined;
46
+ const o = raw;
47
+ if (typeof o.state !== 'string' || !VALIDITY_STATES.includes(o.state))
48
+ return undefined;
49
+ const info = {
50
+ state: o.state,
51
+ validToSource: typeof o.validToSource === 'string' && VALID_TO_SOURCES.includes(o.validToSource)
52
+ ? o.validToSource
53
+ : 'unknown',
54
+ basis: typeof o.basis === 'string' ? o.basis : '',
55
+ at: typeof o.at === 'string' ? o.at : '',
56
+ };
57
+ // 写读成对:写侧每个可选字段都必须在这里成对读回(漏读=静默丢字段,
58
+ // supersede 的 conflictType 曾踩过这个坑)。
59
+ if (typeof o.by === 'string' && o.by)
60
+ info.by = o.by;
61
+ if (typeof o.pairId === 'string' && o.pairId)
62
+ info.pairId = o.pairId;
63
+ if (typeof o.confidence === 'number' && Number.isFinite(o.confidence)) {
64
+ info.confidence = Math.max(0, Math.min(1, o.confidence));
65
+ }
66
+ return info;
67
+ }
68
+ /**
69
+ * 去标注(重判前清旧键用)。返回**新对象**;无键时返回**原引用**——
70
+ * 对未标注记录做 strip 是彻底的 no-op,不产生任何新对象。
71
+ */
72
+ export function stripValidity(metadata) {
73
+ const base = metadata ?? {};
74
+ if (!(VALIDITY_METADATA_KEY in base))
75
+ return base;
76
+ const next = { ...base };
77
+ delete next[VALIDITY_METADATA_KEY];
78
+ return next;
79
+ }
80
+ /**
81
+ * 六态判据(spec §0 表,唯一真源)。
82
+ *
83
+ * 判定次序:退场标记 > 时间轴。marker 存在即 `superseded`/`retired`,
84
+ * 不看 valid_to——"软删遮蔽"与"过时"是正交两轴,marker 是既成事实。
85
+ * persistence 未判定时 **fail-open**:valid_to 未闭合 → `current`;
86
+ * 已闭合(无论来源)→ `expired_suspect`(宁可标注疑,也不静默隐藏)。
87
+ */
88
+ export function classifyValidity(input) {
89
+ const { marker, persistence, validTo, validToSource = 'unknown', now } = input;
90
+ if (marker) {
91
+ return marker.reason === 'manual' || marker.reason === 'obsolete' ? 'retired' : 'superseded';
92
+ }
93
+ const hasValidTo = typeof validTo === 'number' && Number.isFinite(validTo) && validTo > 0;
94
+ const past = hasValidTo && validTo <= now;
95
+ if (persistence === undefined) {
96
+ // 未判定不参与取代/闭合判定,只当普通事实:能判疑,不能判死。
97
+ return past ? 'expired_suspect' : 'current';
98
+ }
99
+ if (persistence === 'o') {
100
+ if (!hasValidTo || !past)
101
+ return 'current';
102
+ return validToSource === 'evidence' ? 'expired' : 'expired_suspect';
103
+ }
104
+ // p(时点)/ s(已结束区间)/ t(无时间性):未退场即"有效-历史",照常可检可注入。
105
+ return 'historical';
106
+ }
107
+ /** 六态里是否属于**退场态**(撤出检索面的那两档)。 */
108
+ export function isExitedState(state) {
109
+ return state === 'superseded' || state === 'retired';
110
+ }
111
+ /** 从记录 metadata 推 validToSource(未标注一律 'unknown',判不出可信过时)。 */
112
+ export function validToSourceOf(metadata) {
113
+ return readValidity(metadata)?.validToSource ?? 'unknown';
114
+ }
115
+ /**
116
+ * 检索出口的时效可见性谓词(门3,工具与面板 RPC 共用一份——防两处漂移)。
117
+ *
118
+ * 存在理由:门1 解除索引遮蔽后,带合法历史/过期 valid_to 的行会进检索命中;
119
+ * "默认只给有效"由出口层这刀维持(与改前"索引天然遮蔽 expired"的输出逐字等价)。
120
+ * 退场两档(superseded/retired)已被门1 索引层挡在命中之外,这里再查一道
121
+ * 只为 `asOf` 回放路径("不复活退场"的显式兑现),不是重复过滤。
122
+ */
123
+ export function passesValidityGate(record, now, opts = {}) {
124
+ // 退场行永不可见(asOf 不复活;命中面本已不含它们,防御性兑现语义)。
125
+ if (readSupersedeMarker(record.metadata) !== undefined)
126
+ return false;
127
+ const asOf = typeof opts.asOf === 'number' && Number.isFinite(opts.asOf) && opts.asOf > 0 ? opts.asOf : undefined;
128
+ if (asOf !== undefined) {
129
+ const from = record.validFrom ?? record.createdAt;
130
+ if (Number.isFinite(from) && from > asOf)
131
+ return false;
132
+ if (record.validTo !== undefined && asOf >= record.validTo)
133
+ return false;
134
+ return true;
135
+ }
136
+ if (opts.includeExpired === true)
137
+ return true;
138
+ // 默认与 includeExpired 两档都只剔"可信 expired";expired_suspect 保留(疑而不隐)。
139
+ return stateOfRecord(record, now) !== 'expired';
140
+ }
141
+ /**
142
+ * 记录现态的**读侧**定性(expiry-tiering 门2/门3 与图谱传播共用):
143
+ * 六态由记录现值 + 当下墙钟**查询期谓词**算出,不吃落库快照——
144
+ * `expired` 随时间自然成立,拿写入时缓存的 state 会把"当时还没过时"当成永真。
145
+ * 标注(`dsh_validity`)只为面板/审计留一份"写当时怎么判的",不是判据本身。
146
+ */
147
+ export function stateOfRecord(record, now) {
148
+ return classifyValidity({
149
+ marker: readSupersedeMarker(record.metadata),
150
+ persistence: record.persistence,
151
+ validTo: record.validTo,
152
+ validToSource: validToSourceOf(record.metadata),
153
+ now,
154
+ });
155
+ }
156
+ /**
157
+ * 被动注入候选的时效过滤(门2 主体,纯函数供单测;调用点在 `hooks/recall.ts`,
158
+ * 仅 `tieringEnabled` 为真时进入——关时逐字现状由调用点门保证)。
159
+ *
160
+ * 判据单源 `classifyValidity`(经 stateOfRecord 现算),不另写 valid_to/marker 比较(计划 §4 约束)。
161
+ * 行缺失 fail-open 保留:宁多注一条也不静默丢记忆。
162
+ */
163
+ export function filterInjectCandidates(candidates, recordOf, now) {
164
+ const kept = [];
165
+ const suspectIds = new Set();
166
+ let exited = 0;
167
+ for (const c of candidates) {
168
+ const r = recordOf(c.id);
169
+ if (r === undefined) {
170
+ kept.push(c);
171
+ continue;
172
+ }
173
+ const state = stateOfRecord(r, now);
174
+ if (state === 'expired')
175
+ continue;
176
+ if (isExitedState(state)) {
177
+ exited++;
178
+ continue;
179
+ }
180
+ if (state === 'expired_suspect')
181
+ suspectIds.add(c.id);
182
+ kept.push(c);
183
+ }
184
+ return { kept, suspectIds, exited };
185
+ }
186
+ /**
187
+ * 写侧标注钩子(expiry-tiering 门2/门3 的写入时机,spec §2.1"派生时机"):
188
+ * 对**新建/重写**的记录就地盖一份 `dsh_validity` 快照(供面板/审计/反查展示,
189
+ * 判定仍以 `stateOfRecord` 现算为准——本快照不是判据)。
190
+ *
191
+ * 直接变异传入 record(与 `familyForType` 兜底同位:写侧拥有刚建的记录对象;
192
+ * 检索命中等共享对象绝不得经本函数改写)。`sourceHint` 只在尚无标注时生效:
193
+ * 管线/工具在源头就知道 valid_to 的证据链,盖得越早越准;汇流口补盖拿不到
194
+ * 源头信息时留 'unknown'(fail-open,只能判疑不能判死)。
195
+ */
196
+ export function stampValidity(record, now, sourceHint) {
197
+ const existing = readValidity(record.metadata);
198
+ const marker = readSupersedeMarker(record.metadata);
199
+ const source = existing?.validToSource ?? sourceHint ?? 'unknown';
200
+ const state = classifyValidity({ marker, persistence: record.persistence, validTo: record.validTo, validToSource: source, now });
201
+ record.metadata = withValidity(record.metadata, {
202
+ state,
203
+ validToSource: source,
204
+ basis: marker ? `marker:${marker.reason}` : `persistence:${record.persistence ?? '?'} validToSource:${source}`,
205
+ at: new Date(now).toISOString(),
206
+ ...(marker?.by ? { by: marker.by } : {}),
207
+ ...(marker?.pairId ? { pairId: marker.pairId } : {}),
208
+ });
209
+ }
@@ -19,6 +19,18 @@ export interface ToolSealRegistry {
19
19
  /** 登记一个工具组的一次性注册器;`make()` 须返回 `ctx.tools.register` 的 disposer。 */
20
20
  reg(group: string, make: () => () => void): void;
21
21
  }
22
+ /**
23
+ * 确定性写面的钩子(graph-write-plane R2/R3,由 index.ts 注入 runner 的公开方法):
24
+ * 工具面不直接握 runner,只拿这两个闭包——入队钩子与强制重投;缺席时写面工具
25
+ * 显式拒答(不开静默半功能:模型看到"已写入图谱会跟随"而实际没入队,比拒答更糟)。
26
+ */
27
+ export interface GraphWriteHooks {
28
+ enqueueGraph: (recordIds: readonly string[]) => void;
29
+ reprojectGraph: (recordIds: readonly string[]) => {
30
+ queued: number;
31
+ jobIds: string[];
32
+ };
33
+ }
22
34
  export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, stores: {
23
35
  /** DRSC 客户端(T8.4 读切):conversation_search 的历史供给面;缺席=显式报错。 */
24
36
  drsc?: DrscClient;
@@ -35,4 +47,6 @@ export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, sto
35
47
  /** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
36
48
  ruminate?: RuminateController,
37
49
  /** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
38
- roomRegistry?: RoomRegistryStore): ToolSealRegistry;
50
+ roomRegistry?: RoomRegistryStore,
51
+ /** 图谱写面钩子(确定性写面计划;缺省 = update/reproject/migrate 的图谱跟随不可用)。 */
52
+ graphWrite?: GraphWriteHooks): ToolSealRegistry;