dsh-prime-memory 0.26.4 → 0.26.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +36785 -134
- package/dist/clients/drsc.d.ts +93 -0
- package/dist/clients/drsc.js +139 -0
- package/dist/contract.d.ts +74 -0
- package/dist/drs-anchor-backfill.d.ts +88 -0
- package/dist/drs-anchor-backfill.js +150 -0
- package/dist/graph/constraints.d.ts +3 -1
- package/dist/graph/constraints.js +4 -2
- package/dist/graph/preview.d.ts +92 -0
- package/dist/graph/preview.js +186 -0
- package/dist/graph/search.d.ts +22 -5
- package/dist/graph/search.js +69 -7
- package/dist/graph/types.d.ts +26 -1
- package/dist/graph/types.js +26 -1
- package/dist/hooks/capture.d.ts +4 -4
- package/dist/hooks/capture.js +34 -27
- package/dist/hooks/recall.js +8 -1
- package/dist/index.js +58 -20
- package/dist/pipeline/graph.d.ts +9 -0
- package/dist/pipeline/graph.js +29 -2
- package/dist/pipeline/runner.d.ts +17 -4
- package/dist/pipeline/runner.js +33 -6
- package/dist/stats.js +30 -1
- package/dist/store/embedding-source.d.ts +1 -3
- package/dist/store/embedding-source.js +6 -13
- package/dist/store/graph-store.d.ts +40 -3
- package/dist/store/graph-store.js +104 -8
- package/dist/store/l0-watermark.d.ts +46 -0
- package/dist/store/l0-watermark.js +93 -0
- package/dist/store/l0.d.ts +12 -42
- package/dist/store/l0.js +2 -208
- package/dist/store/l1.d.ts +12 -9
- package/dist/store/l1.js +37 -16
- package/dist/store/session-recalled.d.ts +9 -3
- package/dist/store/session-recalled.js +24 -5
- package/dist/store/sqlite.d.ts +3 -0
- package/dist/store/sqlite.js +7 -0
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +27 -10
- package/dist/types.d.ts +9 -0
- package/dsh.plugin.json +1 -1
- package/package.json +8 -3
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { GraphEdge, GraphNode, GraphNodeType } from './types.js';
|
|
2
|
+
export interface GraphSeedNode {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
type: GraphNodeType;
|
|
6
|
+
/** active 边双向计数(两端都可见才计入,与 degreeMaps 同口径)。 */
|
|
7
|
+
degree: number;
|
|
8
|
+
/** 其中因果边(constraints.GRAPH_CAUSAL_RELATIONS)计数。 */
|
|
9
|
+
causalDegree: number;
|
|
10
|
+
}
|
|
11
|
+
export interface GraphSeedNeighbor {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: GraphNodeType;
|
|
15
|
+
/** 该条边的关系词(平行多边各占一条,标注各画各的)。 */
|
|
16
|
+
relation: string;
|
|
17
|
+
causal: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface GraphSeedView {
|
|
20
|
+
node: GraphSeedNode;
|
|
21
|
+
/** 因果边优先→邻居度数降序→updatedAt 新先→id 升序;长度 ≤ neighborLimit。 */
|
|
22
|
+
neighbors: GraphSeedNeighbor[];
|
|
23
|
+
/** 邻居超上限被截断的标记(UI 画 "+n" 角标)。 */
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface GraphPreviewStats {
|
|
27
|
+
nodes: number;
|
|
28
|
+
edges: number;
|
|
29
|
+
causalEdges: number;
|
|
30
|
+
/** 未定投影 job 数(pending/running/failed),>0 → UI "构建中 x/y"。 */
|
|
31
|
+
pendingJobs: number;
|
|
32
|
+
/** 全量 job 数(分母)。 */
|
|
33
|
+
jobsTotal: number;
|
|
34
|
+
/** 永久失败(dead)作业数,可选(零漂移先例);>0 → UI 拆三档。 */
|
|
35
|
+
deadJobs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface GraphPreviewResult {
|
|
38
|
+
seeds: GraphSeedView[];
|
|
39
|
+
stats: GraphPreviewStats;
|
|
40
|
+
}
|
|
41
|
+
export interface PreviewOpts {
|
|
42
|
+
/** 给定即只展开该节点为唯一中心(悬挂/不可见 → 空 seeds,不抛)。 */
|
|
43
|
+
seedId?: string;
|
|
44
|
+
seedLimit?: number;
|
|
45
|
+
neighborLimit?: number;
|
|
46
|
+
pendingJobs?: number;
|
|
47
|
+
jobsTotal?: number;
|
|
48
|
+
deadJobs?: number;
|
|
49
|
+
}
|
|
50
|
+
/** S-2:邻接统计——active 边且两端都可见才计度(双向)。 */
|
|
51
|
+
export interface DegreeMaps {
|
|
52
|
+
degree: Map<string, number>;
|
|
53
|
+
causal: Map<string, number>;
|
|
54
|
+
}
|
|
55
|
+
export declare function degreeMaps(nodes: readonly GraphNode[], edges: readonly GraphEdge[]): DegreeMaps;
|
|
56
|
+
/**
|
|
57
|
+
* S-3/S-4:预览主函数。
|
|
58
|
+
* - seedId 视图:直接展开该节点(跳过挑选);不可见/悬挂 → 空 seeds。
|
|
59
|
+
* - 榜单视图:score = degree + causal×WEIGHT;同分链 causal→sources→updatedAt→id↑;
|
|
60
|
+
* 候选落入已选种子的"自身∪邻居"覆盖集则跳过(相邻合并,画布片不重复);
|
|
61
|
+
* 孤立节点(度 0)不入榜——画布只有中心一个点没有信息量。
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildGraphPreview(nodes: readonly GraphNode[], edges: readonly GraphEdge[], opts?: PreviewOpts): GraphPreviewResult;
|
|
64
|
+
/** 全图节点轻量项(星图渲染只需这三字段,facts/sources 不下发——带宽与隐私面同步收紧)。 */
|
|
65
|
+
export interface FullViewNode {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
type: GraphNodeType;
|
|
69
|
+
}
|
|
70
|
+
/** 全图边轻量项(causal 由单源词表判出,渲染侧不再抄词表)。 */
|
|
71
|
+
export interface FullViewEdge {
|
|
72
|
+
from: string;
|
|
73
|
+
to: string;
|
|
74
|
+
relation: string;
|
|
75
|
+
causal: boolean;
|
|
76
|
+
}
|
|
77
|
+
export interface GraphFullView {
|
|
78
|
+
nodes: FullViewNode[];
|
|
79
|
+
edges: FullViewEdge[];
|
|
80
|
+
/** 任一维触顶即 true(截断标记而非硬拒;渲染侧可据此提示"仅画部分")。 */
|
|
81
|
+
truncated: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface FullViewCaps {
|
|
84
|
+
nodeCap?: number;
|
|
85
|
+
edgeCap?: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 全图星图数据块(纯函数,不查库):可见节点轻量投影 × active 边,
|
|
89
|
+
* 双上限截断。**顺序纪律**:节点按入参序截断(调用方 loadGraph 的行序),
|
|
90
|
+
* 悬挂边(端点之一被截掉/不可见)在截断前剔除——星图上不画悬空线。
|
|
91
|
+
*/
|
|
92
|
+
export declare function buildFullView(nodes: readonly GraphNode[], edges: readonly GraphEdge[], caps?: FullViewCaps): GraphFullView;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板图谱预览(读路径纯函数):种子推荐榜 + 指定节点邻域展开。
|
|
3
|
+
*
|
|
4
|
+
* 定位:图谱投影三表的"值得画哪些片"视图筛选,不查库、不落库、无 I/O——
|
|
5
|
+
* job 计数(pendingJobs/jobsTotal)由存储缝(graph-store.buildPreview)传入。
|
|
6
|
+
* 起点推荐有实证底座(2026-10-20 装机审计):关系全落规范词表、因果边 ~24%,
|
|
7
|
+
* 故 score 以 active 度数为主键、因果边加权——纯 top 度会漏"因→果"链。
|
|
8
|
+
*
|
|
9
|
+
* 判据与检索同源(search.ts):候选 = active|disputed 节点 × active 边;
|
|
10
|
+
* 确定性 = 同输入两次调用逐位一致(同分裁决链落到 id 升序兜底)。
|
|
11
|
+
*/
|
|
12
|
+
import { GRAPH_CAUSAL_RELATIONS } from './constraints.js';
|
|
13
|
+
import { GRAPH_PREVIEW_CAUSAL_WEIGHT, GRAPH_PREVIEW_FULL_EDGES, GRAPH_PREVIEW_FULL_NODES, GRAPH_PREVIEW_NEIGHBORS, GRAPH_PREVIEW_SEEDS, } from './types.js';
|
|
14
|
+
const CAUSAL_SET = new Set(GRAPH_CAUSAL_RELATIONS);
|
|
15
|
+
/** 预览唯一判定的节点可见性(与 searchGraphNodes 候选集同式)。 */
|
|
16
|
+
function visible(n) {
|
|
17
|
+
return n.status === 'active' || n.status === 'disputed';
|
|
18
|
+
}
|
|
19
|
+
export function degreeMaps(nodes, edges) {
|
|
20
|
+
const candidate = new Set(nodes.filter(visible).map((n) => n.id));
|
|
21
|
+
const degree = new Map();
|
|
22
|
+
const causal = new Map();
|
|
23
|
+
for (const e of edges) {
|
|
24
|
+
if (e.status !== 'active')
|
|
25
|
+
continue;
|
|
26
|
+
if (!candidate.has(e.fromNodeId) || !candidate.has(e.toNodeId))
|
|
27
|
+
continue;
|
|
28
|
+
const isCausal = CAUSAL_SET.has(e.relation);
|
|
29
|
+
for (const id of [e.fromNodeId, e.toNodeId]) {
|
|
30
|
+
degree.set(id, (degree.get(id) ?? 0) + 1);
|
|
31
|
+
if (isCausal)
|
|
32
|
+
causal.set(id, (causal.get(id) ?? 0) + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { degree, causal };
|
|
36
|
+
}
|
|
37
|
+
/** 种子卡装配:邻居条目 + 因果优先排序 + 截断。 */
|
|
38
|
+
function seedView(center, edges, byId, dm, neighborLimit) {
|
|
39
|
+
const list = [];
|
|
40
|
+
for (const e of edges) {
|
|
41
|
+
if (e.status !== 'active')
|
|
42
|
+
continue;
|
|
43
|
+
let otherId;
|
|
44
|
+
if (e.fromNodeId === center.id)
|
|
45
|
+
otherId = e.toNodeId;
|
|
46
|
+
else if (e.toNodeId === center.id)
|
|
47
|
+
otherId = e.fromNodeId;
|
|
48
|
+
else
|
|
49
|
+
continue;
|
|
50
|
+
const o = byId.get(otherId);
|
|
51
|
+
if (!o || !visible(o))
|
|
52
|
+
continue;
|
|
53
|
+
list.push({
|
|
54
|
+
id: o.id,
|
|
55
|
+
name: o.name,
|
|
56
|
+
type: o.type,
|
|
57
|
+
relation: e.relation,
|
|
58
|
+
causal: CAUSAL_SET.has(e.relation),
|
|
59
|
+
_deg: dm.degree.get(o.id) ?? 0,
|
|
60
|
+
_upd: o.updatedAt,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
list.sort((a, b) => {
|
|
64
|
+
if (a.causal !== b.causal)
|
|
65
|
+
return a.causal ? -1 : 1;
|
|
66
|
+
if (a._deg !== b._deg)
|
|
67
|
+
return b._deg - a._deg;
|
|
68
|
+
if (a._upd !== b._upd)
|
|
69
|
+
return a._upd < b._upd ? -1 : 1; // updatedAt 新(大)先
|
|
70
|
+
return a.id < b.id ? -1 : 1;
|
|
71
|
+
});
|
|
72
|
+
const truncated = list.length > neighborLimit;
|
|
73
|
+
return {
|
|
74
|
+
node: {
|
|
75
|
+
id: center.id,
|
|
76
|
+
name: center.name,
|
|
77
|
+
type: center.type,
|
|
78
|
+
degree: dm.degree.get(center.id) ?? 0,
|
|
79
|
+
causalDegree: dm.causal.get(center.id) ?? 0,
|
|
80
|
+
},
|
|
81
|
+
neighbors: list.slice(0, neighborLimit).map(({ _deg, _upd, ...view }) => view),
|
|
82
|
+
truncated,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* S-3/S-4:预览主函数。
|
|
87
|
+
* - seedId 视图:直接展开该节点(跳过挑选);不可见/悬挂 → 空 seeds。
|
|
88
|
+
* - 榜单视图:score = degree + causal×WEIGHT;同分链 causal→sources→updatedAt→id↑;
|
|
89
|
+
* 候选落入已选种子的"自身∪邻居"覆盖集则跳过(相邻合并,画布片不重复);
|
|
90
|
+
* 孤立节点(度 0)不入榜——画布只有中心一个点没有信息量。
|
|
91
|
+
*/
|
|
92
|
+
export function buildGraphPreview(nodes, edges, opts = {}) {
|
|
93
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
94
|
+
const dm = degreeMaps(nodes, edges);
|
|
95
|
+
const visNodes = nodes.filter(visible);
|
|
96
|
+
const visEdges = edges.filter((e) => e.status === 'active');
|
|
97
|
+
const stats = {
|
|
98
|
+
nodes: visNodes.length,
|
|
99
|
+
edges: visEdges.length,
|
|
100
|
+
causalEdges: visEdges.filter((e) => CAUSAL_SET.has(e.relation)).length,
|
|
101
|
+
pendingJobs: opts.pendingJobs ?? 0,
|
|
102
|
+
jobsTotal: opts.jobsTotal ?? 0,
|
|
103
|
+
...(opts.deadJobs != null && opts.deadJobs > 0 ? { deadJobs: opts.deadJobs } : {}),
|
|
104
|
+
};
|
|
105
|
+
const seedLimit = Math.min(Math.max(Math.floor(opts.seedLimit ?? GRAPH_PREVIEW_SEEDS), 1), 20);
|
|
106
|
+
const neighborLimit = Math.min(Math.max(Math.floor(opts.neighborLimit ?? GRAPH_PREVIEW_NEIGHBORS), 1), 40);
|
|
107
|
+
if (opts.seedId !== undefined) {
|
|
108
|
+
const target = byId.get(opts.seedId);
|
|
109
|
+
if (!target || !visible(target))
|
|
110
|
+
return { seeds: [], stats };
|
|
111
|
+
return { seeds: [seedView(target, edges, byId, dm, neighborLimit)], stats };
|
|
112
|
+
}
|
|
113
|
+
const scored = visNodes
|
|
114
|
+
.map((n) => ({
|
|
115
|
+
n,
|
|
116
|
+
score: (dm.degree.get(n.id) ?? 0) + (dm.causal.get(n.id) ?? 0) * GRAPH_PREVIEW_CAUSAL_WEIGHT,
|
|
117
|
+
}))
|
|
118
|
+
.sort((a, b) => {
|
|
119
|
+
if (a.score !== b.score)
|
|
120
|
+
return b.score - a.score;
|
|
121
|
+
const ca = dm.causal.get(a.n.id) ?? 0;
|
|
122
|
+
const cb = dm.causal.get(b.n.id) ?? 0;
|
|
123
|
+
if (ca !== cb)
|
|
124
|
+
return cb - ca;
|
|
125
|
+
const sa = a.n.sourceRecordIds.length;
|
|
126
|
+
const sb = b.n.sourceRecordIds.length;
|
|
127
|
+
if (sa !== sb)
|
|
128
|
+
return sb - sa;
|
|
129
|
+
if (a.n.updatedAt !== b.n.updatedAt)
|
|
130
|
+
return a.n.updatedAt < b.n.updatedAt ? 1 : -1;
|
|
131
|
+
return a.n.id < b.n.id ? -1 : 1;
|
|
132
|
+
});
|
|
133
|
+
const picked = [];
|
|
134
|
+
const covered = new Set();
|
|
135
|
+
for (const { n, score } of scored) {
|
|
136
|
+
if (picked.length >= seedLimit)
|
|
137
|
+
break;
|
|
138
|
+
if (score === 0)
|
|
139
|
+
continue; // 孤立点不入榜(score 单调降,continue 等价于提前结束)
|
|
140
|
+
if (covered.has(n.id))
|
|
141
|
+
continue; // 相邻合并:已被前一种子的邻域覆盖
|
|
142
|
+
picked.push(n);
|
|
143
|
+
covered.add(n.id);
|
|
144
|
+
for (const e of visEdges) {
|
|
145
|
+
if (e.fromNodeId === n.id)
|
|
146
|
+
covered.add(e.toNodeId);
|
|
147
|
+
else if (e.toNodeId === n.id)
|
|
148
|
+
covered.add(e.fromNodeId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
seeds: picked.map((n) => seedView(n, edges, byId, dm, neighborLimit)),
|
|
153
|
+
stats,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 全图星图数据块(纯函数,不查库):可见节点轻量投影 × active 边,
|
|
158
|
+
* 双上限截断。**顺序纪律**:节点按入参序截断(调用方 loadGraph 的行序),
|
|
159
|
+
* 悬挂边(端点之一被截掉/不可见)在截断前剔除——星图上不画悬空线。
|
|
160
|
+
*/
|
|
161
|
+
export function buildFullView(nodes, edges, caps = {}) {
|
|
162
|
+
const nodeCap = Math.max(1, Math.floor(caps.nodeCap ?? GRAPH_PREVIEW_FULL_NODES));
|
|
163
|
+
const edgeCap = Math.max(1, Math.floor(caps.edgeCap ?? GRAPH_PREVIEW_FULL_EDGES));
|
|
164
|
+
const visibleNodes = nodes.filter(visible);
|
|
165
|
+
const nodesTruncated = visibleNodes.length > nodeCap;
|
|
166
|
+
const drawn = visibleNodes.slice(0, nodeCap);
|
|
167
|
+
const keep = new Set(drawn.map((n) => n.id));
|
|
168
|
+
const outEdges = [];
|
|
169
|
+
let edgesTruncated = false;
|
|
170
|
+
for (const e of edges) {
|
|
171
|
+
if (e.status !== 'active')
|
|
172
|
+
continue;
|
|
173
|
+
if (!keep.has(e.fromNodeId) || !keep.has(e.toNodeId))
|
|
174
|
+
continue;
|
|
175
|
+
if (outEdges.length >= edgeCap) {
|
|
176
|
+
edgesTruncated = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
outEdges.push({ from: e.fromNodeId, to: e.toNodeId, relation: e.relation, causal: CAUSAL_SET.has(e.relation) });
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
nodes: drawn.map((n) => ({ id: n.id, name: n.name, type: n.type })),
|
|
183
|
+
edges: outEdges,
|
|
184
|
+
truncated: nodesTruncated || edgesTruncated,
|
|
185
|
+
};
|
|
186
|
+
}
|
package/dist/graph/search.d.ts
CHANGED
|
@@ -13,15 +13,32 @@ export declare const GRAPH_FIELD_WEIGHTS: {
|
|
|
13
13
|
* 图谱命中 → L1 记录 id 序列(§D 第 3 路 RRF 的输入;纯函数,无 I/O)。
|
|
14
14
|
*
|
|
15
15
|
* 图谱是**可重建派生投影**,节点/fact 的 `sourceRecordIds` 是它回链 L1 的唯一凭证
|
|
16
|
-
* (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名"
|
|
16
|
+
* (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名",分两段:
|
|
17
17
|
*
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
18
|
+
* - **直连段**:命中节点自身 + 其 active facts 的来源(task_9 原语义、原顺序);
|
|
19
|
+
* - **1-hop 段**:命中节点沿 **active 边**的双向邻居(邻居需 active|disputed)的
|
|
20
|
+
* 节点自身来源,按邻居 updatedAt 新→旧追加,整段新增封顶
|
|
21
|
+
* `GRAPH_HOP_RECORD_CAP`——这是图路"关系可达"增量价值的兑现面。
|
|
22
|
+
* hop 段只取邻居节点自身来源,不取 fact 来源(hop 是联想不是证据,噪声从紧)。
|
|
23
|
+
*
|
|
24
|
+
* 两段共用纪律:
|
|
25
|
+
* - **保序**:先出现的记录 = 更高排名(直连段恒先于 hop 段);已被直连段占位的
|
|
26
|
+
* id 不重复计入、也**不吃 hop 名额**(关联额度不被身份来源挤占);
|
|
27
|
+
* - **去重**:同一 L1 记录被多个节点/fact 引用,只占首次出现的位置;
|
|
21
28
|
* - **节点自身来源优先于其 fact 来源**:节点的 sourceRecordIds 是它的身份来源,
|
|
22
29
|
* 而单个 fact 的来源可能引向与该查询无关的旁支记录;
|
|
23
30
|
* - **只取 active fact**:superseded 是历史状态,不该把旧记录拉进当前检索结果;
|
|
24
|
-
* - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"
|
|
31
|
+
* - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理,
|
|
32
|
+
* 不得补空占位(占位会在 RRF 里凭空加分)。
|
|
33
|
+
*
|
|
34
|
+
* edges/nodes 由调用方传入(loadGraph 结果,百~千量级全扫即可;>2k 再考虑预建索引,
|
|
35
|
+
* 见文件头演进阶梯)。族过滤不在本函数:邻居节点可跨族,兜底在 graphLane 尾部的
|
|
36
|
+
* 记录级 family/scope 双闸(宁可漏不可串落在记录上)。
|
|
37
|
+
*/
|
|
38
|
+
export declare function expandHitRecordIds(hits: readonly GraphNodeSearchResult[], nodes: readonly GraphNode[], edges: readonly GraphEdge[]): string[];
|
|
39
|
+
/**
|
|
40
|
+
* 直连段回链(无 hop)——保留 task_9 的原语义形状(既有测试继续钉住);
|
|
41
|
+
* 生产链路一律走 expandHitRecordIds(向量种子并集 + 1-hop)。
|
|
25
42
|
*/
|
|
26
43
|
export declare function graphHitRecordIds(hits: readonly GraphNodeSearchResult[]): string[];
|
|
27
44
|
/**
|
package/dist/graph/search.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* - score 只用于排序,不是事实置信度;无词法命中返回空,不兜底。
|
|
35
35
|
*/
|
|
36
36
|
import { tokenize } from '../util/text.js';
|
|
37
|
-
import { GRAPH_SEARCH_LIMIT_MAX } from './types.js';
|
|
37
|
+
import { GRAPH_HOP_RECORD_CAP, GRAPH_SEARCH_LIMIT_MAX } from './types.js';
|
|
38
38
|
/** 字段权重(检索命中计分;名称最高、关系词最低)。 */
|
|
39
39
|
export const GRAPH_FIELD_WEIGHTS = {
|
|
40
40
|
name: 6,
|
|
@@ -60,21 +60,37 @@ const FIELD_ORDER = ['name', 'aliases', 'tags', 'currentState', 'facts', 'relati
|
|
|
60
60
|
function factValueText(value) {
|
|
61
61
|
return Array.isArray(value) ? value.join(' ') : value;
|
|
62
62
|
}
|
|
63
|
+
/** 节点新度序(updatedAt 新→旧,同值按 id 升序——与检索结果排序同式)。 */
|
|
64
|
+
function byNodeRecency(a, b) {
|
|
65
|
+
return a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : a.id < b.id ? -1 : 1;
|
|
66
|
+
}
|
|
63
67
|
/**
|
|
64
68
|
* 图谱命中 → L1 记录 id 序列(§D 第 3 路 RRF 的输入;纯函数,无 I/O)。
|
|
65
69
|
*
|
|
66
70
|
* 图谱是**可重建派生投影**,节点/fact 的 `sourceRecordIds` 是它回链 L1 的唯一凭证
|
|
67
|
-
* (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名"
|
|
71
|
+
* (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名",分两段:
|
|
72
|
+
*
|
|
73
|
+
* - **直连段**:命中节点自身 + 其 active facts 的来源(task_9 原语义、原顺序);
|
|
74
|
+
* - **1-hop 段**:命中节点沿 **active 边**的双向邻居(邻居需 active|disputed)的
|
|
75
|
+
* 节点自身来源,按邻居 updatedAt 新→旧追加,整段新增封顶
|
|
76
|
+
* `GRAPH_HOP_RECORD_CAP`——这是图路"关系可达"增量价值的兑现面。
|
|
77
|
+
* hop 段只取邻居节点自身来源,不取 fact 来源(hop 是联想不是证据,噪声从紧)。
|
|
68
78
|
*
|
|
69
|
-
*
|
|
70
|
-
* -
|
|
71
|
-
*
|
|
79
|
+
* 两段共用纪律:
|
|
80
|
+
* - **保序**:先出现的记录 = 更高排名(直连段恒先于 hop 段);已被直连段占位的
|
|
81
|
+
* id 不重复计入、也**不吃 hop 名额**(关联额度不被身份来源挤占);
|
|
82
|
+
* - **去重**:同一 L1 记录被多个节点/fact 引用,只占首次出现的位置;
|
|
72
83
|
* - **节点自身来源优先于其 fact 来源**:节点的 sourceRecordIds 是它的身份来源,
|
|
73
84
|
* 而单个 fact 的来源可能引向与该查询无关的旁支记录;
|
|
74
85
|
* - **只取 active fact**:superseded 是历史状态,不该把旧记录拉进当前检索结果;
|
|
75
|
-
* - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"
|
|
86
|
+
* - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理,
|
|
87
|
+
* 不得补空占位(占位会在 RRF 里凭空加分)。
|
|
88
|
+
*
|
|
89
|
+
* edges/nodes 由调用方传入(loadGraph 结果,百~千量级全扫即可;>2k 再考虑预建索引,
|
|
90
|
+
* 见文件头演进阶梯)。族过滤不在本函数:邻居节点可跨族,兜底在 graphLane 尾部的
|
|
91
|
+
* 记录级 family/scope 双闸(宁可漏不可串落在记录上)。
|
|
76
92
|
*/
|
|
77
|
-
export function
|
|
93
|
+
export function expandHitRecordIds(hits, nodes, edges) {
|
|
78
94
|
const seen = new Set();
|
|
79
95
|
const out = [];
|
|
80
96
|
const take = (id) => {
|
|
@@ -83,6 +99,7 @@ export function graphHitRecordIds(hits) {
|
|
|
83
99
|
seen.add(id);
|
|
84
100
|
out.push(id);
|
|
85
101
|
};
|
|
102
|
+
// 直连段(节点身份来源在前,active facts 在后)
|
|
86
103
|
for (const h of hits) {
|
|
87
104
|
for (const id of h.node.sourceRecordIds)
|
|
88
105
|
take(id);
|
|
@@ -93,8 +110,53 @@ export function graphHitRecordIds(hits) {
|
|
|
93
110
|
take(id);
|
|
94
111
|
}
|
|
95
112
|
}
|
|
113
|
+
if (edges.length === 0)
|
|
114
|
+
return out;
|
|
115
|
+
// hop 段:active 边的双向邻居 ∉ 命中集,邻居去重,状态需 active|disputed
|
|
116
|
+
const hitNodeIds = new Set(hits.map((h) => h.node.id));
|
|
117
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
118
|
+
const seenNeighbor = new Set();
|
|
119
|
+
const neighbors = [];
|
|
120
|
+
for (const e of edges) {
|
|
121
|
+
if (e.status !== 'active')
|
|
122
|
+
continue;
|
|
123
|
+
const dirs = [
|
|
124
|
+
[e.fromNodeId, e.toNodeId],
|
|
125
|
+
[e.toNodeId, e.fromNodeId],
|
|
126
|
+
];
|
|
127
|
+
for (const [self, other] of dirs) {
|
|
128
|
+
if (!hitNodeIds.has(self) || hitNodeIds.has(other) || seenNeighbor.has(other))
|
|
129
|
+
continue;
|
|
130
|
+
const n = nodeById.get(other);
|
|
131
|
+
if (!n || (n.status !== 'active' && n.status !== 'disputed'))
|
|
132
|
+
continue;
|
|
133
|
+
seenNeighbor.add(other);
|
|
134
|
+
neighbors.push(n);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
neighbors.sort(byNodeRecency);
|
|
138
|
+
let hopTaken = 0;
|
|
139
|
+
for (const n of neighbors) {
|
|
140
|
+
if (hopTaken >= GRAPH_HOP_RECORD_CAP)
|
|
141
|
+
break;
|
|
142
|
+
for (const id of n.sourceRecordIds) {
|
|
143
|
+
if (seen.has(id))
|
|
144
|
+
continue; // 已被直连段占用:不计入、不吃名额
|
|
145
|
+
if (hopTaken >= GRAPH_HOP_RECORD_CAP)
|
|
146
|
+
break;
|
|
147
|
+
take(id);
|
|
148
|
+
hopTaken++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
96
151
|
return out;
|
|
97
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* 直连段回链(无 hop)——保留 task_9 的原语义形状(既有测试继续钉住);
|
|
155
|
+
* 生产链路一律走 expandHitRecordIds(向量种子并集 + 1-hop)。
|
|
156
|
+
*/
|
|
157
|
+
export function graphHitRecordIds(hits) {
|
|
158
|
+
return expandHitRecordIds(hits, [], []);
|
|
159
|
+
}
|
|
98
160
|
/**
|
|
99
161
|
* 图谱节点检索:返回按 score 降序的命中(最多 limit 条;limit 钳制 1~20)。
|
|
100
162
|
* 候选节点 = active | disputed(superseded/archived 不进检索)。
|
package/dist/graph/types.d.ts
CHANGED
|
@@ -134,9 +134,34 @@ export declare const GRAPH_JOB_BATCH = 8;
|
|
|
134
134
|
export declare const GRAPH_PRIORITY_NEW = 10000;
|
|
135
135
|
/** 存量补投影的优先级(启动/周期补齐;恒低于新蒸馏)。 */
|
|
136
136
|
export declare const GRAPH_PRIORITY_BACKFILL = 100;
|
|
137
|
-
/** 单 job
|
|
137
|
+
/** 单 job 最大尝试次数(永久错误:校验/解析类——3 次不过就 dead)。
|
|
138
|
+
* 瞬态错误(aborted/timeout/网络)走 GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS。 */
|
|
138
139
|
export declare const GRAPH_JOB_MAX_ATTEMPTS = 3;
|
|
140
|
+
/** 瞬态错误(attempt 不消耗到 3 即 dead):网关负载或超时引起的 aborted、
|
|
141
|
+
* terminated、网络抖动、模型空输出——这些与 prompt/数据无关,退避后重试即恢复。
|
|
142
|
+
* 6 次 ≈ 5 轮退避(~30 分钟)覆盖 local-35 负载低谷。 */
|
|
143
|
+
export declare const GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS = 6;
|
|
144
|
+
/** 图谱投影 LLM 单次调用超时(含整条回退链):沿用 llm.timeoutMs(120s) 在 8-record
|
|
145
|
+
* 批 + 上下文节点/边 + 12 关系词 prompt 下真机撞线(14 dead 全是 aborted×13 + terminated×1)。
|
|
146
|
+
* 照 backfill callTimeoutMs 先例给 240s,按「路由数 × 单路由耗时」留余量。 */
|
|
147
|
+
export declare const GRAPH_PROJECTION_CALL_TIMEOUT_MS = 240000;
|
|
139
148
|
/** 失败指数退避基数:第 n 次失败后 nextAttemptAt = now + base × 2^(n-1)。 */
|
|
140
149
|
export declare const GRAPH_JOB_BACKOFF_BASE_MS = 60000;
|
|
141
150
|
/** 单次检索返回上限的硬钳制(工具与 RPC 两侧共用同一钳制值)。 */
|
|
142
151
|
export declare const GRAPH_SEARCH_LIMIT_MAX = 20;
|
|
152
|
+
/** §D 第 3 路路内向量种子的节点条数上限(种子只是联想起点,独立于词法路 limit)。 */
|
|
153
|
+
export declare const GRAPH_VEC_SEED_CAP = 10;
|
|
154
|
+
/** 1-hop 边扩展回链的新增记录数封顶(hop 段专用名额,直连段不占;防 hub 节点邻接爆量)。 */
|
|
155
|
+
export declare const GRAPH_HOP_RECORD_CAP = 64;
|
|
156
|
+
/** 面板图谱预览:种子榜上限(画布片数)。 */
|
|
157
|
+
export declare const GRAPH_PREVIEW_SEEDS = 6;
|
|
158
|
+
/** 面板图谱预览:单种子绘制的邻居上限(超出置 truncated)。 */
|
|
159
|
+
export declare const GRAPH_PREVIEW_NEIGHBORS = 10;
|
|
160
|
+
/** 面板图谱预览:种子评分中因果边的权重(score = degree + causal×此值)——
|
|
161
|
+
* 因果链比共现邻接更值得画,预览侧单源读 GRAPH_CAUSAL_RELATIONS。 */
|
|
162
|
+
export declare const GRAPH_PREVIEW_CAUSAL_WEIGHT = 2;
|
|
163
|
+
/** 全图星图(mode:'full')节点上限:超限截断并置 truncated(截断标记而非硬拒——
|
|
164
|
+
* 星图是展示面,宁可少画几颗也不报错;G6 舒适区 <2000 留余量)。 */
|
|
165
|
+
export declare const GRAPH_PREVIEW_FULL_NODES = 1500;
|
|
166
|
+
/** 全图星图 active 边上限(同一 truncated 语义;悬挂边已在截断前剔除)。 */
|
|
167
|
+
export declare const GRAPH_PREVIEW_FULL_EDGES = 3000;
|
package/dist/graph/types.js
CHANGED
|
@@ -6,9 +6,34 @@ export const GRAPH_JOB_BATCH = 8;
|
|
|
6
6
|
export const GRAPH_PRIORITY_NEW = 10000;
|
|
7
7
|
/** 存量补投影的优先级(启动/周期补齐;恒低于新蒸馏)。 */
|
|
8
8
|
export const GRAPH_PRIORITY_BACKFILL = 100;
|
|
9
|
-
/** 单 job
|
|
9
|
+
/** 单 job 最大尝试次数(永久错误:校验/解析类——3 次不过就 dead)。
|
|
10
|
+
* 瞬态错误(aborted/timeout/网络)走 GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS。 */
|
|
10
11
|
export const GRAPH_JOB_MAX_ATTEMPTS = 3;
|
|
12
|
+
/** 瞬态错误(attempt 不消耗到 3 即 dead):网关负载或超时引起的 aborted、
|
|
13
|
+
* terminated、网络抖动、模型空输出——这些与 prompt/数据无关,退避后重试即恢复。
|
|
14
|
+
* 6 次 ≈ 5 轮退避(~30 分钟)覆盖 local-35 负载低谷。 */
|
|
15
|
+
export const GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS = 6;
|
|
16
|
+
/** 图谱投影 LLM 单次调用超时(含整条回退链):沿用 llm.timeoutMs(120s) 在 8-record
|
|
17
|
+
* 批 + 上下文节点/边 + 12 关系词 prompt 下真机撞线(14 dead 全是 aborted×13 + terminated×1)。
|
|
18
|
+
* 照 backfill callTimeoutMs 先例给 240s,按「路由数 × 单路由耗时」留余量。 */
|
|
19
|
+
export const GRAPH_PROJECTION_CALL_TIMEOUT_MS = 240_000;
|
|
11
20
|
/** 失败指数退避基数:第 n 次失败后 nextAttemptAt = now + base × 2^(n-1)。 */
|
|
12
21
|
export const GRAPH_JOB_BACKOFF_BASE_MS = 60_000;
|
|
13
22
|
/** 单次检索返回上限的硬钳制(工具与 RPC 两侧共用同一钳制值)。 */
|
|
14
23
|
export const GRAPH_SEARCH_LIMIT_MAX = 20;
|
|
24
|
+
/** §D 第 3 路路内向量种子的节点条数上限(种子只是联想起点,独立于词法路 limit)。 */
|
|
25
|
+
export const GRAPH_VEC_SEED_CAP = 10;
|
|
26
|
+
/** 1-hop 边扩展回链的新增记录数封顶(hop 段专用名额,直连段不占;防 hub 节点邻接爆量)。 */
|
|
27
|
+
export const GRAPH_HOP_RECORD_CAP = 64;
|
|
28
|
+
/** 面板图谱预览:种子榜上限(画布片数)。 */
|
|
29
|
+
export const GRAPH_PREVIEW_SEEDS = 6;
|
|
30
|
+
/** 面板图谱预览:单种子绘制的邻居上限(超出置 truncated)。 */
|
|
31
|
+
export const GRAPH_PREVIEW_NEIGHBORS = 10;
|
|
32
|
+
/** 面板图谱预览:种子评分中因果边的权重(score = degree + causal×此值)——
|
|
33
|
+
* 因果链比共现邻接更值得画,预览侧单源读 GRAPH_CAUSAL_RELATIONS。 */
|
|
34
|
+
export const GRAPH_PREVIEW_CAUSAL_WEIGHT = 2;
|
|
35
|
+
/** 全图星图(mode:'full')节点上限:超限截断并置 truncated(截断标记而非硬拒——
|
|
36
|
+
* 星图是展示面,宁可少画几颗也不报错;G6 舒适区 <2000 留余量)。 */
|
|
37
|
+
export const GRAPH_PREVIEW_FULL_NODES = 1500;
|
|
38
|
+
/** 全图星图 active 边上限(同一 truncated 语义;悬挂边已在截断前剔除)。 */
|
|
39
|
+
export const GRAPH_PREVIEW_FULL_EDGES = 3000;
|
package/dist/hooks/capture.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
|
3
3
|
import type { MemoryConfig } from '../config.js';
|
|
4
4
|
import type { MemoryRunner } from '../pipeline/runner.js';
|
|
5
5
|
import type { SessionModeStore } from '../store/session-modes.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { L0WatermarkStore } from '../store/l0-watermark.js';
|
|
7
7
|
import type { LiveSettingsHandle } from '../settings.js';
|
|
8
8
|
import type { MemoryLogger } from '../types.js';
|
|
9
9
|
export declare function isCaptureRelevant(type: string): boolean;
|
|
@@ -20,10 +20,10 @@ export declare class CaptureBuffers {
|
|
|
20
20
|
takeTurn(sid: string, turn: number): SessionEvent[];
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* 注册 L0
|
|
24
|
-
*
|
|
23
|
+
* 注册 L0 捕获。返回冲刷函数(dispose 序 await;写停后无排队写,恒立即完成)。
|
|
24
|
+
* capture 关闭时返回 undefined。
|
|
25
25
|
*/
|
|
26
|
-
export declare function registerCapture(ctx: Context, cfg: MemoryConfig, runner: MemoryRunner,
|
|
26
|
+
export declare function registerCapture(ctx: Context, cfg: MemoryConfig, runner: MemoryRunner, watermark: L0WatermarkStore, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore): (() => Promise<void>) | undefined;
|
|
27
27
|
/**
|
|
28
28
|
* 缓冲上限裁剪(防御性;RELEVANT_TYPES 过滤后基本不可达)。
|
|
29
29
|
* 铁律:进行中轮次(turn/start 之后、turn/end 之前)的事件绝不裁——
|
package/dist/hooks/capture.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* L0 捕获 Hook:订阅 session/event,按轮次缓冲 user/assistant 消息,
|
|
3
|
-
* turn/end 时清洗并交给 Runner
|
|
3
|
+
* turn/end 时清洗并交给 Runner 蒸馏 + 登记水位(T8.4 写停:正文不落库,
|
|
4
|
+
* 历史读面由 DRSC 投影承接)。
|
|
4
5
|
*
|
|
5
6
|
* 冷启动保护:插件激活时间之前的事件不捕获(防止恢复会话时倾倒全部历史)。
|
|
6
7
|
*/
|
|
@@ -86,20 +87,34 @@ export class CaptureBuffers {
|
|
|
86
87
|
return turnEvents;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
90
|
+
/** 该批消息里已知的最大 step(水位 last_step 登记用;无 step 锚点返回 undefined)。 */
|
|
91
|
+
function lastStepOf(messages) {
|
|
92
|
+
let step;
|
|
93
|
+
for (const m of messages) {
|
|
94
|
+
const s = m.anchor?.step;
|
|
95
|
+
if (typeof s === 'number' && (step === undefined || s > step))
|
|
96
|
+
step = s;
|
|
97
|
+
}
|
|
98
|
+
return step;
|
|
99
|
+
}
|
|
89
100
|
/**
|
|
90
|
-
* 注册 L0
|
|
91
|
-
*
|
|
101
|
+
* 注册 L0 捕获。返回冲刷函数(dispose 序 await;写停后无排队写,恒立即完成)。
|
|
102
|
+
* capture 关闭时返回 undefined。
|
|
92
103
|
*/
|
|
93
|
-
export function registerCapture(ctx, cfg, runner,
|
|
104
|
+
export function registerCapture(ctx, cfg, runner, watermark, logger, live, modes) {
|
|
94
105
|
if (!cfg.capture.enabled)
|
|
95
106
|
return;
|
|
96
107
|
const startFloor = Date.now();
|
|
97
108
|
const buffers = new CaptureBuffers();
|
|
98
109
|
// task_8 一次性提示标记:宿主读日志服务不可得时只提示一次(防每次 resume 刷屏)
|
|
99
110
|
let warnedRecoveryUnavailable = false;
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
111
|
+
// T8.4 写停(drsc-redact-memory-wiring R6):l0 正文三表(l0_conversations/l0_fts/
|
|
112
|
+
// l0_vec)与 JSONL 事实源一并退役——历史读面由 DRSC 投影供给,本地只剩水位表
|
|
113
|
+
// 做 §A 恢复幂等自检。蒸馏直传不经 DRSC(capture 单点脱敏照旧)。
|
|
114
|
+
// 水位登记是同步 SQLite 写,事件循环天然串行,无需再排队。
|
|
115
|
+
const settleTurn = (sid, turn, messages) => {
|
|
116
|
+
watermark.record(sid, turn, lastStepOf(messages));
|
|
117
|
+
};
|
|
103
118
|
ctx.on('session/event', (session, event) => {
|
|
104
119
|
try {
|
|
105
120
|
const s = live.get();
|
|
@@ -146,12 +161,9 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
146
161
|
logger.info(`[memory] L0 捕获 turn=${turn} ${messages.length} 条(${Object.entries(roles)
|
|
147
162
|
.map(([k, v]) => `${k}=${v}`)
|
|
148
163
|
.join('/')} ,session=${sid},mode=${mode})`);
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.then(() => l0.append(sid, messages))
|
|
153
|
-
.then(() => logger.info(`[memory] L0 落盘 ${n} 条`))
|
|
154
|
-
.catch((err) => logger.warn(`[memory] L0 落盘失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
164
|
+
// 写停:水位登记即"落盘"(正文由 DRSC 投影承接);蒸馏直传照旧
|
|
165
|
+
settleTurn(sid, turn, messages);
|
|
166
|
+
logger.info(`[memory] L0 写停登记 turn=${turn}(正文不落库,历史面经 DRSC)`);
|
|
155
167
|
runner.enqueue(sid, messages, mode);
|
|
156
168
|
}
|
|
157
169
|
}
|
|
@@ -199,16 +211,16 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
199
211
|
}
|
|
200
212
|
events = degraded;
|
|
201
213
|
}
|
|
202
|
-
const
|
|
203
|
-
const brackets = foldRecoverableTurns(events,
|
|
214
|
+
const watermarkTurn = watermark.maxTurn(sid);
|
|
215
|
+
const brackets = foldRecoverableTurns(events, watermarkTurn);
|
|
204
216
|
if (brackets.length === 0)
|
|
205
217
|
return;
|
|
206
218
|
let recovered = 0;
|
|
207
219
|
let skipped = 0;
|
|
208
220
|
let total = 0;
|
|
209
221
|
for (const bracket of brackets) {
|
|
210
|
-
if (
|
|
211
|
-
//
|
|
222
|
+
if (watermark.hasAny(sid, bracket.turn)) {
|
|
223
|
+
// 水位表判"该轮已捕获":整轮跳过,宁少收不重复(≡ 退役前行存在性检查)
|
|
212
224
|
skipped += 1;
|
|
213
225
|
continue;
|
|
214
226
|
}
|
|
@@ -217,25 +229,20 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
217
229
|
continue;
|
|
218
230
|
recovered += 1;
|
|
219
231
|
total += messages.length;
|
|
220
|
-
|
|
221
|
-
l0Queue = l0Queue
|
|
222
|
-
.then(() => l0.append(sid, messages))
|
|
223
|
-
.then(() => logger.info(`[memory] L0 恢复落盘 ${n} 条`))
|
|
224
|
-
.catch((err) => logger.warn(`[memory] L0 恢复落盘失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
232
|
+
settleTurn(sid, bracket.turn, messages);
|
|
225
233
|
runner.enqueue(sid, messages, mode);
|
|
226
234
|
}
|
|
227
235
|
if (recovered > 0 || skipped > 0) {
|
|
228
|
-
logger.info(`[memory] resume 对账:水位线 ${
|
|
236
|
+
logger.info(`[memory] resume 对账:水位线 ${watermarkTurn ?? '无'} → 恢复 ${recovered} 轮 ${total} 条,幂等跳过 ${skipped} 轮(session=${sid})`);
|
|
229
237
|
}
|
|
230
238
|
}
|
|
231
239
|
catch (err) {
|
|
232
240
|
logger.warn(`[memory] resume 恢复失败(不阻塞会话启动): ${err instanceof Error ? err.message : String(err)}`);
|
|
233
241
|
}
|
|
234
242
|
});
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
return () => l0Queue;
|
|
243
|
+
// 冲刷:写停后无排队写(水位登记同步完成),恒立即完成。保留异步签名
|
|
244
|
+
// 供 dispose 序 await(index.ts 停机顺序不变)。
|
|
245
|
+
return async () => { };
|
|
239
246
|
}
|
|
240
247
|
/**
|
|
241
248
|
* 缓冲上限裁剪(防御性;RELEVANT_TYPES 过滤后基本不可达)。
|