dsh-prime-memory 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +28 -0
- package/CHANGELOG.ja.md +30 -0
- package/CHANGELOG.ko.md +30 -0
- package/CHANGELOG.md +1220 -0
- package/ENGINEERING-NOTES.md +452 -0
- package/INSTALL.en.md +92 -0
- package/INSTALL.ja.md +92 -0
- package/INSTALL.ko.md +92 -0
- package/INSTALL.md +92 -0
- package/LICENSE +21 -0
- package/README.en.md +458 -0
- package/README.ja.md +306 -0
- package/README.ko.md +306 -0
- package/README.md +425 -0
- package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
- package/assets/changelog/0.8.9/01-panel.png +0 -0
- package/assets/changelog/0.8.9/02-halo.png +0 -0
- package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
- package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
- package/assets/img/EmbeddingSource.png +0 -0
- package/assets/img/Hero.png +0 -0
- package/assets/img/Layers.png +0 -0
- package/assets/img/MemoryTools.png +0 -0
- package/assets/img/Modes.png +0 -0
- package/assets/img/ToolTrajectory.png +0 -0
- package/assets/img/ui-dark.jpg +0 -0
- package/assets/img/ui-light.jpg +0 -0
- package/assets/readme/bench-dialog.svg +70 -0
- package/assets/readme/bench-workflow.svg +79 -0
- package/assets/readme/flow.svg +189 -0
- package/assets/readme/storage.svg +115 -0
- package/cordis.patch.yml +16 -0
- package/dist/bench-control.d.ts +34 -0
- package/dist/bench-control.js +16 -0
- package/dist/client.js +4293 -0
- package/dist/config.d.ts +683 -0
- package/dist/config.js +129 -0
- package/dist/contract.d.ts +820 -0
- package/dist/contract.js +1 -0
- package/dist/embedding-worker.cjs +176 -0
- package/dist/graph/apply.d.ts +37 -0
- package/dist/graph/apply.js +270 -0
- package/dist/graph/constraints.d.ts +47 -0
- package/dist/graph/constraints.js +38 -0
- package/dist/graph/search.d.ts +16 -0
- package/dist/graph/search.js +115 -0
- package/dist/graph/types.d.ts +142 -0
- package/dist/graph/types.js +14 -0
- package/dist/hooks/capture.d.ts +32 -0
- package/dist/hooks/capture.js +194 -0
- package/dist/hooks/recall.d.ts +63 -0
- package/dist/hooks/recall.js +429 -0
- package/dist/index.d.ts +534 -0
- package/dist/index.js +344 -0
- package/dist/llm-usage.d.ts +26 -0
- package/dist/llm-usage.js +37 -0
- package/dist/llm.d.ts +153 -0
- package/dist/llm.js +530 -0
- package/dist/pipeline/graph.d.ts +35 -0
- package/dist/pipeline/graph.js +104 -0
- package/dist/pipeline/l1.d.ts +19 -0
- package/dist/pipeline/l1.js +271 -0
- package/dist/pipeline/l2.d.ts +13 -0
- package/dist/pipeline/l2.js +83 -0
- package/dist/pipeline/l3.d.ts +15 -0
- package/dist/pipeline/l3.js +78 -0
- package/dist/pipeline/rebuild.d.ts +61 -0
- package/dist/pipeline/rebuild.js +307 -0
- package/dist/pipeline/ruminate.d.ts +89 -0
- package/dist/pipeline/ruminate.js +298 -0
- package/dist/pipeline/runner.d.ts +167 -0
- package/dist/pipeline/runner.js +638 -0
- package/dist/pipeline/trigger.d.ts +40 -0
- package/dist/pipeline/trigger.js +75 -0
- package/dist/prompts/graph-projection.d.ts +70 -0
- package/dist/prompts/graph-projection.js +167 -0
- package/dist/prompts/l1-dedup.d.ts +22 -0
- package/dist/prompts/l1-dedup.js +251 -0
- package/dist/prompts/l1-extraction.d.ts +22 -0
- package/dist/prompts/l1-extraction.js +457 -0
- package/dist/prompts/persona.d.ts +23 -0
- package/dist/prompts/persona.js +240 -0
- package/dist/prompts/scene.d.ts +32 -0
- package/dist/prompts/scene.js +414 -0
- package/dist/runtime-package-lock.json +982 -0
- package/dist/settings.d.ts +50 -0
- package/dist/settings.js +355 -0
- package/dist/stats.d.ts +109 -0
- package/dist/stats.js +929 -0
- package/dist/store/bm25.d.ts +19 -0
- package/dist/store/bm25.js +63 -0
- package/dist/store/cost-ledger.d.ts +75 -0
- package/dist/store/cost-ledger.js +171 -0
- package/dist/store/download-queue.d.ts +79 -0
- package/dist/store/download-queue.js +424 -0
- package/dist/store/embedding-source.d.ts +118 -0
- package/dist/store/embedding-source.js +443 -0
- package/dist/store/embedding.d.ts +90 -0
- package/dist/store/embedding.js +206 -0
- package/dist/store/graph-store.d.ts +94 -0
- package/dist/store/graph-store.js +641 -0
- package/dist/store/l0.d.ts +40 -0
- package/dist/store/l0.js +197 -0
- package/dist/store/l1.d.ts +93 -0
- package/dist/store/l1.js +297 -0
- package/dist/store/local-embedding.d.ts +89 -0
- package/dist/store/local-embedding.js +227 -0
- package/dist/store/model-catalog.d.ts +48 -0
- package/dist/store/model-catalog.js +81 -0
- package/dist/store/occupancy.d.ts +30 -0
- package/dist/store/occupancy.js +134 -0
- package/dist/store/pending.d.ts +36 -0
- package/dist/store/pending.js +103 -0
- package/dist/store/persona.d.ts +15 -0
- package/dist/store/persona.js +60 -0
- package/dist/store/recall-dedupe.d.ts +26 -0
- package/dist/store/recall-dedupe.js +138 -0
- package/dist/store/runtime-installer.d.ts +59 -0
- package/dist/store/runtime-installer.js +243 -0
- package/dist/store/scenes.d.ts +24 -0
- package/dist/store/scenes.js +160 -0
- package/dist/store/search-utils.d.ts +38 -0
- package/dist/store/search-utils.js +100 -0
- package/dist/store/session-modes.d.ts +35 -0
- package/dist/store/session-modes.js +144 -0
- package/dist/store/sqlite.d.ts +246 -0
- package/dist/store/sqlite.js +1491 -0
- package/dist/store/state.d.ts +41 -0
- package/dist/store/state.js +72 -0
- package/dist/token-cost.d.ts +23 -0
- package/dist/token-cost.js +185 -0
- package/dist/tools/index.d.ts +34 -0
- package/dist/tools/index.js +758 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +38 -0
- package/dist/util/context-occupancy.d.ts +68 -0
- package/dist/util/context-occupancy.js +92 -0
- package/dist/util/filelog.d.ts +6 -0
- package/dist/util/filelog.js +108 -0
- package/dist/util/io.d.ts +18 -0
- package/dist/util/io.js +97 -0
- package/dist/util/recall-budget.d.ts +31 -0
- package/dist/util/recall-budget.js +84 -0
- package/dist/util/sanitize.d.ts +11 -0
- package/dist/util/sanitize.js +67 -0
- package/dist/util/text.d.ts +16 -0
- package/dist/util/text.js +61 -0
- package/dist/util/tokenizer.d.ts +9 -0
- package/dist/util/tokenizer.js +50 -0
- package/dsh.plugin.json +22 -0
- package/package.json +118 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图谱存储层(graph_nodes / graph_edges / graph_projection_jobs 三张域表 +
|
|
3
|
+
* graph_job_records / graph_projected_records 两张 job 索引表)。
|
|
4
|
+
*
|
|
5
|
+
* 仿 CostLedger 的边界:与检索引擎零关系,唯一耦合是共享同一个 node:sqlite
|
|
6
|
+
* 连接;但降级姿态强于 CostLedger 先例——GraphStore.init 独立 try/catch,初始化
|
|
7
|
+
* 失败只让图谱域 no-op(检索返空/入队返 0/claim 返 null),绝不传染整库降级
|
|
8
|
+
* (图谱是纯投影,不值得为此停捕获/蒸馏)。
|
|
9
|
+
*
|
|
10
|
+
* job 队列语义:
|
|
11
|
+
* - 去重下推 SQL:in-flight 去重查 graph_job_records(PK record_id),已完成去重
|
|
12
|
+
* 查 graph_projected_records——都是索引点查,不做全 job 扫描;
|
|
13
|
+
* - 优先级不倒挂:新蒸馏=10000 > 存量补投影=100;claim 按 priority DESC 取;
|
|
14
|
+
* - claim 只认当前 projectorVersion;attempts 在 claim 时 +1,≥3 的 job 不再被
|
|
15
|
+
* claim(fail 时转 dead 收尾);
|
|
16
|
+
* - LLM 永不进事务:claim(置 running)与 complete(读 scope → 纯函数 apply →
|
|
17
|
+
* 写回)是两个独立事务缝,complete 用 BEGIN IMMEDIATE 全程持写锁;
|
|
18
|
+
* - 来源全缺失的 job 判 dead-不可重试,不向调用方抛错。
|
|
19
|
+
*/
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
21
|
+
import { applyGraphProjection } from '../graph/apply.js';
|
|
22
|
+
import { searchGraphNodes } from '../graph/search.js';
|
|
23
|
+
import { GRAPH_JOB_BACKOFF_BASE_MS, GRAPH_JOB_BATCH, GRAPH_JOB_MAX_ATTEMPTS, GRAPH_PRIORITY_BACKFILL, GRAPH_PROJECTOR_VERSION, } from '../graph/types.js';
|
|
24
|
+
import { normPersistence } from '../types.js';
|
|
25
|
+
const TAG = '[memory][graph]';
|
|
26
|
+
/** 坏 JSON 容忍解析:图谱表列损坏只损失该行派生信息,不抛。 */
|
|
27
|
+
function parseJsonSafe(raw, fallback) {
|
|
28
|
+
if (!raw)
|
|
29
|
+
return fallback;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** L1 行(MemoryDb 同款列)→ MemoryRecord;timestamp_str 逗号连接 ISO 契约与主库一致。 */
|
|
38
|
+
function rowToRecord(r) {
|
|
39
|
+
const family = r.family;
|
|
40
|
+
return {
|
|
41
|
+
id: String(r.record_id),
|
|
42
|
+
content: String(r.content),
|
|
43
|
+
type: String(r.type ?? ''),
|
|
44
|
+
priority: Number(r.priority ?? 50),
|
|
45
|
+
scene_name: String(r.scene_name ?? ''),
|
|
46
|
+
timestamps: String(r.timestamp_str ?? '')
|
|
47
|
+
.split(',')
|
|
48
|
+
.map((t) => Date.parse(t))
|
|
49
|
+
.filter((t) => !Number.isNaN(t)),
|
|
50
|
+
createdAt: Date.parse(String(r.created_time ?? '')) || 0,
|
|
51
|
+
updatedAt: Date.parse(String(r.updated_time ?? '')) || 0,
|
|
52
|
+
version: Number(r.version ?? 0),
|
|
53
|
+
metadata: parseJsonSafe(r.metadata_json, {}),
|
|
54
|
+
family: family === 'work' || family === 'chat' ? family : undefined,
|
|
55
|
+
// 时间增强列(与主库同源):图谱时间锚优先取它们
|
|
56
|
+
validFrom: r.valid_from ? Date.parse(String(r.valid_from)) || undefined : undefined,
|
|
57
|
+
validTo: r.valid_to ? Date.parse(String(r.valid_to)) || undefined : undefined,
|
|
58
|
+
persistence: normPersistence(r.persistence),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* L1 列清单(基础列 + 按库的实际形状裁剪的时间增强列)。
|
|
63
|
+
*
|
|
64
|
+
* 时间增强列由 MemoryDb 的增量迁移补齐,但 GraphStore 也可能被挂到一个尚未迁移的
|
|
65
|
+
* 库上(手建库/旧库/独立测试夹具)——此时写死列名会直接 `no such column` 让 claim 失败。
|
|
66
|
+
* 故按 `PRAGMA table_info` 现查现拼,缺列就退回基础列(时间轴回落到 metadata.activity_*)。
|
|
67
|
+
*/
|
|
68
|
+
function l1SelectCols(db) {
|
|
69
|
+
const base = 'record_id, content, type, priority, scene_name, version, timestamp_str, created_time, updated_time, metadata_json, family';
|
|
70
|
+
try {
|
|
71
|
+
const cols = new Set(db.prepare('PRAGMA table_info(l1_records)').all().map((r) => r.name));
|
|
72
|
+
const extra = ['valid_from', 'valid_to', 'persistence'].filter((c) => cols.has(c));
|
|
73
|
+
return extra.length > 0 ? `${base}, ${extra.join(', ')}` : base;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return base;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function rowToNode(r) {
|
|
80
|
+
return {
|
|
81
|
+
id: String(r.node_id),
|
|
82
|
+
name: String(r.name),
|
|
83
|
+
type: r.type,
|
|
84
|
+
aliases: parseJsonSafe(r.aliases_json, []),
|
|
85
|
+
tags: parseJsonSafe(r.tags_json, []),
|
|
86
|
+
currentState: String(r.current_state ?? ''),
|
|
87
|
+
facts: parseJsonSafe(r.facts_json, []),
|
|
88
|
+
status: r.status,
|
|
89
|
+
confidence: Number(r.confidence ?? 0.8),
|
|
90
|
+
sourceRecordIds: parseJsonSafe(r.source_record_ids_json, []),
|
|
91
|
+
families: parseJsonSafe(r.families_json, []),
|
|
92
|
+
createdAt: String(r.created_time ?? ''),
|
|
93
|
+
updatedAt: String(r.updated_time ?? ''),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function rowToEdge(r) {
|
|
97
|
+
return {
|
|
98
|
+
id: String(r.edge_id),
|
|
99
|
+
fromNodeId: String(r.from_node_id),
|
|
100
|
+
toNodeId: String(r.to_node_id),
|
|
101
|
+
relation: String(r.relation ?? ''),
|
|
102
|
+
status: r.status,
|
|
103
|
+
validFrom: r.valid_from || undefined,
|
|
104
|
+
validTo: r.valid_to || undefined,
|
|
105
|
+
confidence: Number(r.confidence ?? 0.8),
|
|
106
|
+
sourceRecordIds: parseJsonSafe(r.source_record_ids_json, []),
|
|
107
|
+
createdAt: String(r.created_time ?? ''),
|
|
108
|
+
updatedAt: String(r.updated_time ?? ''),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function rowToJob(r) {
|
|
112
|
+
return {
|
|
113
|
+
id: r.job_id,
|
|
114
|
+
sourceRecordIds: parseJsonSafe(r.source_record_ids_json, []),
|
|
115
|
+
priority: r.priority,
|
|
116
|
+
projectorVersion: r.projector_version,
|
|
117
|
+
status: r.status,
|
|
118
|
+
attempts: r.attempts,
|
|
119
|
+
nextAttemptAt: r.next_attempt_at,
|
|
120
|
+
...(r.error ? { error: r.error } : {}),
|
|
121
|
+
createdAt: r.created_time,
|
|
122
|
+
updatedAt: r.updated_time,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** 分块 IN 点查(沿用主库 chunkIds 的保守上限)。 */
|
|
126
|
+
function chunkOf(ids, size = 900) {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (let i = 0; i < ids.length; i += size)
|
|
129
|
+
out.push(ids.slice(i, i + size));
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
export class GraphStore {
|
|
133
|
+
db = null;
|
|
134
|
+
logger;
|
|
135
|
+
stmtInsertNode;
|
|
136
|
+
stmtInsertEdge;
|
|
137
|
+
/** init 是否就绪(未就绪 = 图谱域整体 no-op,不抛错不传染)。 */
|
|
138
|
+
get ready() {
|
|
139
|
+
return this.db !== null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 建表 + 语句缓存(MemoryDb.initSchema 内调用)。任何一步失败都只告警并保持
|
|
143
|
+
* 未就绪——图谱域整体降级 no-op,检索主链路(L0/L1/FTS/向量)不受影响。
|
|
144
|
+
*/
|
|
145
|
+
init(db, logger) {
|
|
146
|
+
this.logger = logger;
|
|
147
|
+
try {
|
|
148
|
+
db.exec(`
|
|
149
|
+
CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
150
|
+
node_id TEXT PRIMARY KEY,
|
|
151
|
+
name TEXT NOT NULL,
|
|
152
|
+
type TEXT NOT NULL,
|
|
153
|
+
aliases_json TEXT NOT NULL DEFAULT '[]',
|
|
154
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
155
|
+
current_state TEXT NOT NULL DEFAULT '',
|
|
156
|
+
facts_json TEXT NOT NULL DEFAULT '[]',
|
|
157
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
158
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
159
|
+
source_record_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
160
|
+
families_json TEXT NOT NULL DEFAULT '[]',
|
|
161
|
+
created_time TEXT NOT NULL DEFAULT '',
|
|
162
|
+
updated_time TEXT NOT NULL DEFAULT ''
|
|
163
|
+
);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_graph_nodes_status ON graph_nodes(status);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_graph_nodes_updated ON graph_nodes(updated_time);
|
|
166
|
+
CREATE TABLE IF NOT EXISTS graph_edges (
|
|
167
|
+
edge_id TEXT PRIMARY KEY,
|
|
168
|
+
from_node_id TEXT NOT NULL,
|
|
169
|
+
to_node_id TEXT NOT NULL,
|
|
170
|
+
relation TEXT NOT NULL DEFAULT '',
|
|
171
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
172
|
+
valid_from TEXT DEFAULT '',
|
|
173
|
+
valid_to TEXT DEFAULT '',
|
|
174
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
175
|
+
source_record_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
176
|
+
created_time TEXT NOT NULL DEFAULT '',
|
|
177
|
+
updated_time TEXT NOT NULL DEFAULT ''
|
|
178
|
+
);
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_from ON graph_edges(from_node_id);
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_status ON graph_edges(status);
|
|
181
|
+
CREATE TABLE IF NOT EXISTS graph_projection_jobs (
|
|
182
|
+
job_id TEXT PRIMARY KEY,
|
|
183
|
+
source_record_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
184
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
185
|
+
projector_version INTEGER NOT NULL DEFAULT 1,
|
|
186
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
187
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
188
|
+
next_attempt_at INTEGER,
|
|
189
|
+
error TEXT,
|
|
190
|
+
created_time TEXT NOT NULL DEFAULT '',
|
|
191
|
+
updated_time TEXT NOT NULL DEFAULT ''
|
|
192
|
+
);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_graph_jobs_pick ON graph_projection_jobs(status, priority);
|
|
194
|
+
CREATE TABLE IF NOT EXISTS graph_job_records (
|
|
195
|
+
record_id TEXT PRIMARY KEY,
|
|
196
|
+
job_id TEXT NOT NULL
|
|
197
|
+
);
|
|
198
|
+
CREATE TABLE IF NOT EXISTS graph_projected_records (
|
|
199
|
+
record_id TEXT PRIMARY KEY,
|
|
200
|
+
projector_version INTEGER NOT NULL
|
|
201
|
+
);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_graph_projected_version ON graph_projected_records(projector_version);
|
|
203
|
+
`);
|
|
204
|
+
this.stmtInsertNode = db.prepare(`
|
|
205
|
+
INSERT INTO graph_nodes (
|
|
206
|
+
node_id, name, type, aliases_json, tags_json, current_state, facts_json,
|
|
207
|
+
status, confidence, source_record_ids_json, families_json, created_time, updated_time
|
|
208
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
209
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
210
|
+
name=excluded.name, type=excluded.type, aliases_json=excluded.aliases_json,
|
|
211
|
+
tags_json=excluded.tags_json, current_state=excluded.current_state,
|
|
212
|
+
facts_json=excluded.facts_json, status=excluded.status, confidence=excluded.confidence,
|
|
213
|
+
source_record_ids_json=excluded.source_record_ids_json, families_json=excluded.families_json,
|
|
214
|
+
updated_time=excluded.updated_time
|
|
215
|
+
`);
|
|
216
|
+
this.stmtInsertEdge = db.prepare(`
|
|
217
|
+
INSERT INTO graph_edges (
|
|
218
|
+
edge_id, from_node_id, to_node_id, relation, status, valid_from, valid_to,
|
|
219
|
+
confidence, source_record_ids_json, created_time, updated_time
|
|
220
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
221
|
+
ON CONFLICT(edge_id) DO UPDATE SET
|
|
222
|
+
from_node_id=excluded.from_node_id, to_node_id=excluded.to_node_id,
|
|
223
|
+
relation=excluded.relation, status=excluded.status, valid_from=excluded.valid_from,
|
|
224
|
+
valid_to=excluded.valid_to, confidence=excluded.confidence,
|
|
225
|
+
source_record_ids_json=excluded.source_record_ids_json, updated_time=excluded.updated_time
|
|
226
|
+
`);
|
|
227
|
+
this.db = db;
|
|
228
|
+
logger?.info(`${TAG} 图谱表就绪(5 表,projectorVersion=${GRAPH_PROJECTOR_VERSION})`);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
// 独立降级:图谱域 no-op,不向上抛(强于 costLedger 的冒泡语义——图谱无检索主链路关键)
|
|
232
|
+
this.db = null;
|
|
233
|
+
logger?.warn(`${TAG} 图谱表初始化失败,图谱功能停用(主存储不受影响): ${err instanceof Error ? err.message : String(err)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** 插件停机时清空连接引用(dispose 序调用,防悬空引用)。 */
|
|
237
|
+
close() {
|
|
238
|
+
this.db = null;
|
|
239
|
+
}
|
|
240
|
+
/** 统一事务边界(immediate 供 complete 全程持写锁)。 */
|
|
241
|
+
tx(fn, immediate = false) {
|
|
242
|
+
const db = this.db;
|
|
243
|
+
db.exec(immediate ? 'BEGIN IMMEDIATE' : 'BEGIN');
|
|
244
|
+
try {
|
|
245
|
+
const result = fn();
|
|
246
|
+
db.exec('COMMIT');
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
try {
|
|
251
|
+
db.exec('ROLLBACK');
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
/* ignore */
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** 事务内的 upsert 体(upsertTouched 与 complete 共用;调用方负责事务)。 */
|
|
260
|
+
upsertTouchedInTx(nodes, edges) {
|
|
261
|
+
const now = new Date().toISOString();
|
|
262
|
+
for (const n of nodes) {
|
|
263
|
+
this.stmtInsertNode.run(n.id, n.name, n.type, JSON.stringify(n.aliases), JSON.stringify(n.tags ?? []), n.currentState, JSON.stringify(n.facts), n.status, n.confidence, JSON.stringify(n.sourceRecordIds), JSON.stringify(n.families), n.createdAt || now, n.updatedAt || now);
|
|
264
|
+
}
|
|
265
|
+
for (const e of edges) {
|
|
266
|
+
this.stmtInsertEdge.run(e.id, e.fromNodeId, e.toNodeId, e.relation, e.status, e.validFrom ?? '', e.validTo ?? '', e.confidence, JSON.stringify(e.sourceRecordIds), e.createdAt || now, e.updatedAt || now);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** 全量读图谱(apply scope 与检索的统一入口;图谱量级为可重建投影,百~千级)。 */
|
|
270
|
+
loadGraph() {
|
|
271
|
+
if (!this.db)
|
|
272
|
+
return { nodes: [], edges: [] };
|
|
273
|
+
try {
|
|
274
|
+
return this.loadGraphInTx();
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
this.logger?.warn(`${TAG} 图谱读取失败(返回空): ${err instanceof Error ? err.message : String(err)}`);
|
|
278
|
+
return { nodes: [], edges: [] };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** 事务内的全图读取(complete 用;调用方负责事务)。 */
|
|
282
|
+
loadGraphInTx() {
|
|
283
|
+
const db = this.db;
|
|
284
|
+
const nodes = db.prepare('SELECT * FROM graph_nodes').all().map(rowToNode);
|
|
285
|
+
const edges = db.prepare('SELECT * FROM graph_edges').all().map(rowToEdge);
|
|
286
|
+
return { nodes, edges };
|
|
287
|
+
}
|
|
288
|
+
/** 事务内按 id 装载 L1 记录(claim 与 complete 共用;调用方负责事务)。 */
|
|
289
|
+
loadRecordsInTx(ids) {
|
|
290
|
+
const db = this.db;
|
|
291
|
+
const records = [];
|
|
292
|
+
const cols = l1SelectCols(db);
|
|
293
|
+
for (const chunk of chunkOf(ids)) {
|
|
294
|
+
const ph = chunk.map(() => '?').join(',');
|
|
295
|
+
for (const r of db
|
|
296
|
+
.prepare(`SELECT ${cols} FROM l1_records WHERE record_id IN (${ph})`)
|
|
297
|
+
.all(...chunk)) {
|
|
298
|
+
records.push(rowToRecord(r));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return records;
|
|
302
|
+
}
|
|
303
|
+
/** 单节点详情(expand 用;不存在/未就绪返回 null)。 */
|
|
304
|
+
getNode(id) {
|
|
305
|
+
if (!this.db)
|
|
306
|
+
return null;
|
|
307
|
+
try {
|
|
308
|
+
const row = this.db.prepare('SELECT * FROM graph_nodes WHERE node_id = ?').get(id);
|
|
309
|
+
return row ? rowToNode(row) : null;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** 与某节点相连的 active 边(expand 用;悬挂 id 返回空数组,不解析不抛)。 */
|
|
316
|
+
edgesOf(nodeId) {
|
|
317
|
+
if (!this.db)
|
|
318
|
+
return [];
|
|
319
|
+
try {
|
|
320
|
+
const rows = this.db
|
|
321
|
+
.prepare("SELECT * FROM graph_edges WHERE (from_node_id = ? OR to_node_id = ?) AND status = 'active'")
|
|
322
|
+
.all(nodeId, nodeId);
|
|
323
|
+
return rows.map(rowToEdge);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 图谱检索(纯函数 searchGraphNodes 的存储缝;可选族过滤)。
|
|
331
|
+
* families 非空时只返回本族衍生的节点(档位隔离;无族信息节点一律不可见——
|
|
332
|
+
* 宁可漏不可串)。降级/异常返回空数组。
|
|
333
|
+
*/
|
|
334
|
+
searchNodes(query, limit, families) {
|
|
335
|
+
if (!this.db)
|
|
336
|
+
return [];
|
|
337
|
+
const { nodes, edges } = this.loadGraph();
|
|
338
|
+
const filtered = families && families.length > 0
|
|
339
|
+
? nodes.filter((n) => n.families.some((f) => families.includes(f)))
|
|
340
|
+
: nodes;
|
|
341
|
+
return searchGraphNodes(filtered, edges, query, limit);
|
|
342
|
+
}
|
|
343
|
+
// ============================
|
|
344
|
+
// 投影 job 队列
|
|
345
|
+
// ============================
|
|
346
|
+
newJobId() {
|
|
347
|
+
return `gjob_${Date.now()}_${randomBytes(3).toString('hex')}`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* 投影入队(按 GRAPH_JOB_BATCH 分片成多个 job;去重下推 SQL):
|
|
351
|
+
* 已有在途 mapping(pending/running/failed 退避中)或已按当前版本完成投影的
|
|
352
|
+
* 记录跳过。返回实际新建的 job 数。
|
|
353
|
+
*/
|
|
354
|
+
queueGraphProjection(recordIds, priority) {
|
|
355
|
+
if (!this.db || recordIds.length === 0)
|
|
356
|
+
return 0;
|
|
357
|
+
try {
|
|
358
|
+
const db = this.db;
|
|
359
|
+
const ids = [...new Set(recordIds)];
|
|
360
|
+
// 下推去重:在途 mapping ∪ 已完成投影,都是索引点查
|
|
361
|
+
const blocked = new Set();
|
|
362
|
+
for (const chunk of chunkOf(ids)) {
|
|
363
|
+
const ph = chunk.map(() => '?').join(',');
|
|
364
|
+
for (const row of db.prepare(`SELECT record_id FROM graph_job_records WHERE record_id IN (${ph})`).all(...chunk)) {
|
|
365
|
+
blocked.add(row.record_id);
|
|
366
|
+
}
|
|
367
|
+
for (const row of db
|
|
368
|
+
.prepare(`SELECT record_id FROM graph_projected_records WHERE record_id IN (${ph}) AND projector_version = ?`)
|
|
369
|
+
.all(...chunk, GRAPH_PROJECTOR_VERSION)) {
|
|
370
|
+
blocked.add(row.record_id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const fresh = ids.filter((id) => !blocked.has(id));
|
|
374
|
+
if (fresh.length === 0)
|
|
375
|
+
return 0;
|
|
376
|
+
const now = new Date().toISOString();
|
|
377
|
+
return this.tx(() => {
|
|
378
|
+
let created = 0;
|
|
379
|
+
for (let i = 0; i < fresh.length; i += GRAPH_JOB_BATCH) {
|
|
380
|
+
const batch = fresh.slice(i, i + GRAPH_JOB_BATCH);
|
|
381
|
+
const jobId = this.newJobId();
|
|
382
|
+
db.prepare(`INSERT INTO graph_projection_jobs (job_id, source_record_ids_json, priority, projector_version, status, attempts, created_time, updated_time)
|
|
383
|
+
VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)`).run(jobId, JSON.stringify(batch), priority, GRAPH_PROJECTOR_VERSION, now, now);
|
|
384
|
+
for (const id of batch) {
|
|
385
|
+
db.prepare('INSERT OR REPLACE INTO graph_job_records (record_id, job_id) VALUES (?, ?)').run(id, jobId);
|
|
386
|
+
}
|
|
387
|
+
created++;
|
|
388
|
+
}
|
|
389
|
+
return created;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
this.logger?.warn(`${TAG} 投影入队失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 取下一个可执行 job 并置 running(attempts +1):只认当前 projectorVersion,
|
|
399
|
+
* attempts 封顶与退避窗口在 WHERE 里过滤;来源全缺失 → job 判 dead 返 null
|
|
400
|
+
* (不可重试不抛)。部分缺失时 job 收缩到真实存在的记录子集。
|
|
401
|
+
*/
|
|
402
|
+
claimNext() {
|
|
403
|
+
if (!this.db)
|
|
404
|
+
return null;
|
|
405
|
+
try {
|
|
406
|
+
const db = this.db;
|
|
407
|
+
const nowMs = Date.now();
|
|
408
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
409
|
+
return this.tx(() => {
|
|
410
|
+
const row = db
|
|
411
|
+
.prepare(`SELECT * FROM graph_projection_jobs
|
|
412
|
+
WHERE projector_version = ? AND attempts < ?
|
|
413
|
+
AND status IN ('pending', 'failed')
|
|
414
|
+
AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
415
|
+
ORDER BY priority DESC, created_time ASC LIMIT 1`)
|
|
416
|
+
.get(GRAPH_PROJECTOR_VERSION, GRAPH_JOB_MAX_ATTEMPTS, nowMs);
|
|
417
|
+
if (!row)
|
|
418
|
+
return null;
|
|
419
|
+
db.prepare(`UPDATE graph_projection_jobs SET status = 'running', attempts = attempts + 1, error = NULL, updated_time = ? WHERE job_id = ?`).run(nowIso, row.job_id);
|
|
420
|
+
const job = rowToJob({ ...row, status: 'running', attempts: row.attempts + 1, error: null });
|
|
421
|
+
const records = this.loadRecordsInTx(job.sourceRecordIds);
|
|
422
|
+
if (records.length === 0) {
|
|
423
|
+
// 来源全缺失:判 dead-不可重试,放掉 mapping 占位(不抛错)
|
|
424
|
+
db.prepare(`UPDATE graph_projection_jobs SET status = 'dead', error = ?, updated_time = ? WHERE job_id = ?`).run('来源记录已全部删除', nowIso, row.job_id);
|
|
425
|
+
db.prepare('DELETE FROM graph_job_records WHERE job_id = ?').run(row.job_id);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
if (records.length < job.sourceRecordIds.length) {
|
|
429
|
+
// 部分缺失:job 收缩到真实存在的来源(缺失部分无法回溯,不参与校验)
|
|
430
|
+
const keep = new Set(records.map((r) => r.id));
|
|
431
|
+
job.sourceRecordIds = job.sourceRecordIds.filter((id) => keep.has(id));
|
|
432
|
+
db.prepare('UPDATE graph_projection_jobs SET source_record_ids_json = ?, updated_time = ? WHERE job_id = ?').run(JSON.stringify(job.sourceRecordIds), nowIso, row.job_id);
|
|
433
|
+
}
|
|
434
|
+
return { job, records };
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
this.logger?.warn(`${TAG} claim 投影任务失败(本轮跳过): ${err instanceof Error ? err.message : String(err)}`);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 提交投影结果(单事务,BEGIN IMMEDIATE):读全图 scope → 纯函数 apply(硬
|
|
444
|
+
* 校验)→ 写回 touched 行 → job 置 completed + 登记已投影记录 + 放掉 mapping。
|
|
445
|
+
* job 非 running 态(已完成/已 dead/已回收)一律幂等 no-op;任何一步抛错整体
|
|
446
|
+
* 回滚,不留半写。
|
|
447
|
+
*/
|
|
448
|
+
complete(jobId, result, opts) {
|
|
449
|
+
if (!this.db)
|
|
450
|
+
return;
|
|
451
|
+
try {
|
|
452
|
+
const db = this.db;
|
|
453
|
+
const now = opts?.now ?? new Date().toISOString();
|
|
454
|
+
const idFactory = opts?.idFactory ?? ((prefix) => `${prefix}_${Date.now()}_${randomBytes(3).toString('hex')}`);
|
|
455
|
+
this.tx(() => {
|
|
456
|
+
const row = db.prepare('SELECT * FROM graph_projection_jobs WHERE job_id = ?').get(jobId);
|
|
457
|
+
if (!row)
|
|
458
|
+
return;
|
|
459
|
+
if (row.status !== 'running')
|
|
460
|
+
return; // 幂等:completed/dead/回收中的重复提交无害跳过
|
|
461
|
+
const wanted = parseJsonSafe(row.source_record_ids_json, []);
|
|
462
|
+
const records = this.loadRecordsInTx(wanted);
|
|
463
|
+
const { nodes, edges } = this.loadGraphInTx();
|
|
464
|
+
const outcome = applyGraphProjection({
|
|
465
|
+
nodes,
|
|
466
|
+
edges,
|
|
467
|
+
records,
|
|
468
|
+
result,
|
|
469
|
+
allowedRecordIds: new Set(records.map((r) => r.id)),
|
|
470
|
+
now,
|
|
471
|
+
idFactory,
|
|
472
|
+
});
|
|
473
|
+
const touchedNodes = nodes.filter((n) => outcome.nodeIds.includes(n.id));
|
|
474
|
+
const touchedEdges = edges.filter((e) => outcome.edgeIds.includes(e.id));
|
|
475
|
+
this.upsertTouchedInTx(touchedNodes, touchedEdges);
|
|
476
|
+
db.prepare(`UPDATE graph_projection_jobs SET status = 'completed', error = NULL, updated_time = ? WHERE job_id = ?`).run(now, jobId);
|
|
477
|
+
for (const id of wanted) {
|
|
478
|
+
db.prepare('INSERT OR REPLACE INTO graph_projected_records (record_id, projector_version) VALUES (?, ?)').run(id, GRAPH_PROJECTOR_VERSION);
|
|
479
|
+
}
|
|
480
|
+
db.prepare('DELETE FROM graph_job_records WHERE job_id = ?').run(jobId);
|
|
481
|
+
if (outcome.dropped > 0) {
|
|
482
|
+
this.logger?.warn(`${TAG} 投影校验丢弃 ${outcome.dropped} 条无来源/非法提案(job=${jobId})`);
|
|
483
|
+
}
|
|
484
|
+
this.logger?.info(`${TAG} 投影完成(job=${jobId},来源 ${wanted.length} 条 → 节点 ${touchedNodes.length} / 边 ${touchedEdges.length})`);
|
|
485
|
+
}, true);
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
// 提交失败:job 仍在 running 态,下次启动 recoverRunning 回收重投
|
|
489
|
+
this.logger?.warn(`${TAG} 投影提交失败(待启动回收重投): ${err instanceof Error ? err.message : String(err)}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* 投影失败收尾:attempts 已在 claim 时 +1——封顶转 dead(放掉 mapping,允许
|
|
494
|
+
* 重新入队);未封顶转 failed + 指数退避 nextAttemptAt(mapping 保留防重复入队)。
|
|
495
|
+
*/
|
|
496
|
+
fail(jobId, error) {
|
|
497
|
+
if (!this.db)
|
|
498
|
+
return;
|
|
499
|
+
try {
|
|
500
|
+
const db = this.db;
|
|
501
|
+
const nowMs = Date.now();
|
|
502
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
503
|
+
this.tx(() => {
|
|
504
|
+
const row = db.prepare('SELECT attempts FROM graph_projection_jobs WHERE job_id = ?').get(jobId);
|
|
505
|
+
if (!row)
|
|
506
|
+
return;
|
|
507
|
+
if (row.attempts >= GRAPH_JOB_MAX_ATTEMPTS) {
|
|
508
|
+
db.prepare(`UPDATE graph_projection_jobs SET status = 'dead', error = ?, updated_time = ? WHERE job_id = ?`).run(error.slice(0, 500), nowIso, jobId);
|
|
509
|
+
db.prepare('DELETE FROM graph_job_records WHERE job_id = ?').run(jobId);
|
|
510
|
+
this.logger?.warn(`${TAG} 投影任务达重试上限,标记 dead(job=${jobId}): ${error.slice(0, 200)}`);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const backoffMs = GRAPH_JOB_BACKOFF_BASE_MS * 2 ** (row.attempts - 1);
|
|
514
|
+
db.prepare(`UPDATE graph_projection_jobs SET status = 'failed', error = ?, next_attempt_at = ?, updated_time = ? WHERE job_id = ?`).run(error.slice(0, 500), nowMs + backoffMs, nowIso, jobId);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
this.logger?.warn(`${TAG} 投影失败收尾异常(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/** 启动回收:上次进程退出时卡在 running 的 job 放回 pending(dispose 缝不永久卡批)。 */
|
|
523
|
+
recoverRunning() {
|
|
524
|
+
if (!this.db)
|
|
525
|
+
return 0;
|
|
526
|
+
try {
|
|
527
|
+
const r = this.db
|
|
528
|
+
.prepare(`UPDATE graph_projection_jobs SET status = 'pending', updated_time = ? WHERE status = 'running'`)
|
|
529
|
+
.run(new Date().toISOString());
|
|
530
|
+
const n = Number(r.changes ?? 0);
|
|
531
|
+
if (n > 0)
|
|
532
|
+
this.logger?.info(`${TAG} 启动回收 ${n} 个 running 投影任务 → pending`);
|
|
533
|
+
return n;
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/** 最近 job 列表(诊断/面板用;未就绪返回空)。 */
|
|
540
|
+
listJobs(limit = 50) {
|
|
541
|
+
if (!this.db)
|
|
542
|
+
return [];
|
|
543
|
+
try {
|
|
544
|
+
return this.db
|
|
545
|
+
.prepare('SELECT * FROM graph_projection_jobs ORDER BY created_time DESC LIMIT ?')
|
|
546
|
+
.all(limit).map(rowToJob);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* 存量补投影:从未投影(当前版本)且无在途 mapping 的 L1 记录里按创建时间
|
|
554
|
+
* 升序取最多 limit 条分片入队(优先级 GRAPH_PRIORITY_BACKFILL,恒低于新蒸馏)。
|
|
555
|
+
* 返回新建 job 数。
|
|
556
|
+
*/
|
|
557
|
+
queueMissing(limit) {
|
|
558
|
+
if (!this.db || limit <= 0)
|
|
559
|
+
return 0;
|
|
560
|
+
try {
|
|
561
|
+
const rows = this.db
|
|
562
|
+
.prepare(`SELECT r.record_id FROM l1_records r
|
|
563
|
+
WHERE NOT EXISTS (SELECT 1 FROM graph_projected_records p WHERE p.record_id = r.record_id AND p.projector_version = ?)
|
|
564
|
+
AND NOT EXISTS (SELECT 1 FROM graph_job_records m WHERE m.record_id = r.record_id)
|
|
565
|
+
ORDER BY r.created_time ASC LIMIT ?`)
|
|
566
|
+
.all(GRAPH_PROJECTOR_VERSION, Math.min(limit, 9000));
|
|
567
|
+
return this.queueGraphProjection(rows.map((r) => r.record_id), GRAPH_PRIORITY_BACKFILL);
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
this.logger?.warn(`${TAG} 存量补投影扫描失败(本轮跳过): ${err instanceof Error ? err.message : String(err)}`);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// ============================
|
|
575
|
+
// 删除传播(B10)
|
|
576
|
+
// ============================
|
|
577
|
+
/**
|
|
578
|
+
* 删除传播(L1 批量删除后的惰性墓碑):来源在 L1 已全部不存在的 active/disputed
|
|
579
|
+
* 节点与边标 archived(保留行,expand 可见墓碑)。挂在 MemoryDb.deleteL1Batch
|
|
580
|
+
* 之后;按 L1 存活集判定(而非本次删除集合),跨多次删除与历史孤儿一并收敛。
|
|
581
|
+
*/
|
|
582
|
+
markSourcesDeleted(deletedIds) {
|
|
583
|
+
if (!this.db || deletedIds.length === 0)
|
|
584
|
+
return;
|
|
585
|
+
try {
|
|
586
|
+
const { nodes, edges } = this.loadGraph();
|
|
587
|
+
// 候选来源全集 → L1 存活点查(chunked IN):有任一来源存活即不标
|
|
588
|
+
const sourceIds = new Set();
|
|
589
|
+
for (const n of nodes)
|
|
590
|
+
for (const id of n.sourceRecordIds)
|
|
591
|
+
sourceIds.add(id);
|
|
592
|
+
for (const e of edges)
|
|
593
|
+
for (const id of e.sourceRecordIds)
|
|
594
|
+
sourceIds.add(id);
|
|
595
|
+
if (sourceIds.size === 0)
|
|
596
|
+
return;
|
|
597
|
+
const alive = new Set();
|
|
598
|
+
for (const chunk of chunkOf([...sourceIds])) {
|
|
599
|
+
const ph = chunk.map(() => '?').join(',');
|
|
600
|
+
for (const row of this.db
|
|
601
|
+
.prepare(`SELECT record_id FROM l1_records WHERE record_id IN (${ph})`)
|
|
602
|
+
.all(...chunk)) {
|
|
603
|
+
alive.add(row.record_id);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const dead = (ids) => ids.length > 0 && ids.every((id) => !alive.has(id));
|
|
607
|
+
const now = new Date().toISOString();
|
|
608
|
+
const deadNodes = nodes.filter((n) => (n.status === 'active' || n.status === 'disputed') && dead(n.sourceRecordIds));
|
|
609
|
+
const deadEdges = edges.filter((e) => (e.status === 'active' || e.status === 'disputed') && dead(e.sourceRecordIds));
|
|
610
|
+
if (deadNodes.length === 0 && deadEdges.length === 0)
|
|
611
|
+
return;
|
|
612
|
+
this.tx(() => {
|
|
613
|
+
for (const n of deadNodes) {
|
|
614
|
+
this.db.prepare(`UPDATE graph_nodes SET status = 'archived', updated_time = ? WHERE node_id = ?`).run(now, n.id);
|
|
615
|
+
}
|
|
616
|
+
for (const e of deadEdges) {
|
|
617
|
+
this.db.prepare(`UPDATE graph_edges SET status = 'archived', updated_time = ? WHERE edge_id = ?`).run(now, e.id);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
this.logger?.info(`${TAG} 删除传播:${deadNodes.length} 节点 / ${deadEdges.length} 边来源已全部删除,标 archived`);
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
this.logger?.warn(`${TAG} 删除传播失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/** 清空全部图谱数据(L1 重建时调用——图谱是 L1 的投影,记录清空即图谱作废)。 */
|
|
627
|
+
resetAll() {
|
|
628
|
+
if (!this.db)
|
|
629
|
+
return;
|
|
630
|
+
try {
|
|
631
|
+
this.tx(() => {
|
|
632
|
+
for (const table of ['graph_nodes', 'graph_edges', 'graph_projection_jobs', 'graph_job_records', 'graph_projected_records']) {
|
|
633
|
+
this.db.exec(`DELETE FROM ${table}`);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
this.logger?.warn(`${TAG} 图谱清空失败(忽略): ${err instanceof Error ? err.message : String(err)}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ConversationMessage, L0MessageRecord, MemoryLogger } from '../types.js';
|
|
2
|
+
import { type EmbeddingService } from './embedding.js';
|
|
3
|
+
import { type MemoryDb } from './sqlite.js';
|
|
4
|
+
export declare class L0Store {
|
|
5
|
+
private readonly db;
|
|
6
|
+
private readonly dir;
|
|
7
|
+
private readonly legacyDir;
|
|
8
|
+
private readonly helper;
|
|
9
|
+
private embedSvc;
|
|
10
|
+
private readonly logger?;
|
|
11
|
+
constructor(dataDir: string, db: MemoryDb, embed?: EmbeddingService, logger?: MemoryLogger);
|
|
12
|
+
init(): Promise<void>;
|
|
13
|
+
/** 旧版 l0/*.jsonl 一次性导入检索库,成功后目录改名 l0.imported/。 */
|
|
14
|
+
private importLegacy;
|
|
15
|
+
append(sessionId: string, messages: ConversationMessage[]): Promise<void>;
|
|
16
|
+
/** 今日已捕获消息数(SQL 计数,不再读整文件)。 */
|
|
17
|
+
countToday(): Promise<number>;
|
|
18
|
+
/** 该会话累计已捕获消息数(session-stats 数据源;索引 COUNT)。 */
|
|
19
|
+
countBySession(sessionId: string): Promise<number>;
|
|
20
|
+
/** 该会话最近 n 条消息(时间升序;蒸馏背景参考用,按会话现查——ADR-0003)。 */
|
|
21
|
+
recentBySession(sessionId: string, limit: number): Promise<ConversationMessage[]>;
|
|
22
|
+
/** 检索:FTS + 向量 hybrid(RRF 融合),返回按相关性排序的消息。 */
|
|
23
|
+
search(query: string, limit: number): Promise<L0MessageRecord[]>;
|
|
24
|
+
/** 活切换嵌入源:同步换底层服务(嵌入源三态切换用)。 */
|
|
25
|
+
setEmbeddingService(svc: EmbeddingService): void;
|
|
26
|
+
/**
|
|
27
|
+
* 增量重嵌入(同 L1Store.reindex:只补缺失向量,零向量记 skipped 并入 skip 集,
|
|
28
|
+
* 不算失败、不阻塞同步标记——保证补齐判据收敛)。onProgress/shouldCancel
|
|
29
|
+
* 供活切换的进度展示与取消。
|
|
30
|
+
*/
|
|
31
|
+
reindex(opts?: {
|
|
32
|
+
onProgress?: (done: number, total: number) => void;
|
|
33
|
+
shouldCancel?: () => boolean;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
written: number;
|
|
36
|
+
failed: number;
|
|
37
|
+
skipped: number;
|
|
38
|
+
cancelled?: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
}
|