@memory-river/core 0.2.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.
Files changed (86) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +222 -0
  3. package/README.zh-TW.md +186 -0
  4. package/dist/api.d.ts +100 -0
  5. package/dist/api.js +156 -0
  6. package/dist/cognition/causal-attribution.d.ts +36 -0
  7. package/dist/cognition/causal-attribution.js +239 -0
  8. package/dist/cognition/causal-engine.d.ts +105 -0
  9. package/dist/cognition/causal-engine.js +150 -0
  10. package/dist/cognition/conflict-detector.d.ts +39 -0
  11. package/dist/cognition/conflict-detector.js +193 -0
  12. package/dist/cognition/global-working-memory.d.ts +53 -0
  13. package/dist/cognition/global-working-memory.js +211 -0
  14. package/dist/cognition/hooks-engine.d.ts +99 -0
  15. package/dist/cognition/hooks-engine.js +672 -0
  16. package/dist/cognition/ralph-core.d.ts +28 -0
  17. package/dist/cognition/ralph-core.js +104 -0
  18. package/dist/distill/concentrator-adapter.d.ts +167 -0
  19. package/dist/distill/concentrator-adapter.js +1876 -0
  20. package/dist/engine.d.ts +402 -0
  21. package/dist/engine.js +2254 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +3 -0
  24. package/dist/lifecycle/cleanup-engine.d.ts +80 -0
  25. package/dist/lifecycle/cleanup-engine.js +162 -0
  26. package/dist/lifecycle/cleanup-state.d.ts +34 -0
  27. package/dist/lifecycle/cleanup-state.js +50 -0
  28. package/dist/lifecycle/night-consolidation.d.ts +102 -0
  29. package/dist/lifecycle/night-consolidation.js +640 -0
  30. package/dist/lifecycle/night-recovery.d.ts +40 -0
  31. package/dist/lifecycle/night-recovery.js +107 -0
  32. package/dist/paths.d.ts +17 -0
  33. package/dist/paths.js +16 -0
  34. package/dist/pipeline/capsule-bridge.d.ts +35 -0
  35. package/dist/pipeline/capsule-bridge.js +86 -0
  36. package/dist/pipeline/compact-request.d.ts +30 -0
  37. package/dist/pipeline/compact-request.js +66 -0
  38. package/dist/pipeline/inbox-watcher.d.ts +112 -0
  39. package/dist/pipeline/inbox-watcher.js +1039 -0
  40. package/dist/ports.d.ts +29 -0
  41. package/dist/ports.js +1 -0
  42. package/dist/providers/embedder-v5.d.ts +46 -0
  43. package/dist/providers/embedder-v5.js +155 -0
  44. package/dist/providers/ollama-embedding.d.ts +25 -0
  45. package/dist/providers/ollama-embedding.js +166 -0
  46. package/dist/retrieval/abstractness-judge.d.ts +14 -0
  47. package/dist/retrieval/abstractness-judge.js +87 -0
  48. package/dist/retrieval/coverage-selection.d.ts +3 -0
  49. package/dist/retrieval/coverage-selection.js +53 -0
  50. package/dist/retrieval/cross-encoder-gate.d.ts +40 -0
  51. package/dist/retrieval/cross-encoder-gate.js +239 -0
  52. package/dist/retrieval/retriever-v4.d.ts +78 -0
  53. package/dist/retrieval/retriever-v4.js +1200 -0
  54. package/dist/skills/validate.d.ts +6 -0
  55. package/dist/skills/validate.js +69 -0
  56. package/dist/storage.d.ts +19 -0
  57. package/dist/storage.js +54 -0
  58. package/dist/store/aux-table-maintenance.d.ts +5 -0
  59. package/dist/store/aux-table-maintenance.js +64 -0
  60. package/dist/store/graph-enumerator.d.ts +21 -0
  61. package/dist/store/graph-enumerator.js +185 -0
  62. package/dist/store/graph-store.d.ts +107 -0
  63. package/dist/store/graph-store.js +478 -0
  64. package/dist/store/status-manager.d.ts +44 -0
  65. package/dist/store/status-manager.js +235 -0
  66. package/dist/store/store-v4.d.ts +339 -0
  67. package/dist/store/store-v4.js +2871 -0
  68. package/dist/transcript/keyword-search.d.ts +9 -0
  69. package/dist/transcript/keyword-search.js +67 -0
  70. package/dist/transcript/rehydrate-keyword.d.ts +6 -0
  71. package/dist/transcript/rehydrate-keyword.js +29 -0
  72. package/dist/transcript/rehydrate.d.ts +33 -0
  73. package/dist/transcript/rehydrate.js +285 -0
  74. package/dist/transcript/transcript-archive.d.ts +46 -0
  75. package/dist/transcript/transcript-archive.js +516 -0
  76. package/dist/types.d.ts +409 -0
  77. package/dist/types.js +104 -0
  78. package/dist/util/bounded-map.d.ts +1 -0
  79. package/dist/util/bounded-map.js +8 -0
  80. package/dist/util/rate-limiter.d.ts +12 -0
  81. package/dist/util/rate-limiter.js +54 -0
  82. package/dist/util/session-identity.d.ts +65 -0
  83. package/dist/util/session-identity.js +227 -0
  84. package/dist/util/util-hash.d.ts +1 -0
  85. package/dist/util/util-hash.js +4 -0
  86. package/package.json +59 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Graph Store Engine — 知識圖譜三元組儲存
3
+ * memory-river
4
+ *
5
+ * 在 LanceDB 中建立獨立的 `graph_triples` table,儲存實體關係三元組。
6
+ * 使用 ANN 向量搜尋支援圖譜語意查詢。
7
+ *
8
+ * 設計原則:
9
+ * - 與 MemoryStore 共用同一組 LanceDB 連線(SSD 持久化 + RAM 加速)
10
+ * - 寫入時同步 embed 三元組文字(subject + relation + object)
11
+ * - 查詢時用 findRelatedEntities 做 ANN 相似度搜尋,擴展 Hook trigger
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { recordAuxTableWrite } from "./aux-table-maintenance.js";
15
+ const GRAPH_TABLE_NAME = "graph_triples";
16
+ const SSD_FAILURE_THRESHOLD = 5;
17
+ const DEFAULT_SSD_RECOVERY_PROBE_INTERVAL_MS = 60_000;
18
+ let lancedbImportPromise = null;
19
+ const loadLanceDB = async () => {
20
+ if (!lancedbImportPromise) {
21
+ lancedbImportPromise = import("@lancedb/lancedb");
22
+ }
23
+ return await lancedbImportPromise;
24
+ };
25
+ // H4: 共用 LanceDB 重試工具(提取自 store-v4.ts)
26
+ async function lancedbRetry(operationName, fn, maxRetries = 5) {
27
+ let attempt = 1;
28
+ const MAX_TOTAL_BACKOFF_MS = 5000;
29
+ let totalWaited = 0;
30
+ while (true) {
31
+ try {
32
+ return await fn();
33
+ }
34
+ catch (err) {
35
+ const errMsg = err.message || String(err);
36
+ if (errMsg.includes('Commit conflict') || errMsg.includes('concurrent commit')) {
37
+ if (attempt >= maxRetries || totalWaited >= MAX_TOTAL_BACKOFF_MS) {
38
+ throw new Error(`[GraphStore] ${operationName} 遭遇 Commit Conflict,超過重試上限 (${maxRetries} 次 / ${MAX_TOTAL_BACKOFF_MS}ms): ${errMsg}`);
39
+ }
40
+ const backoff = Math.min(Math.floor(Math.random() * 200) + attempt * 150, MAX_TOTAL_BACKOFF_MS - totalWaited);
41
+ console.warn(`[GraphStore] ${operationName} encountered a concurrency conflict; waiting ${backoff}ms before retry attempt ${attempt}...`);
42
+ await new Promise(r => setTimeout(r, backoff));
43
+ totalWaited += backoff;
44
+ attempt++;
45
+ }
46
+ else {
47
+ throw err;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ export class GraphStore {
53
+ ssdRecoveryProbeIntervalMs;
54
+ ramDb = null;
55
+ ramTable = null;
56
+ ssdDb = null;
57
+ ssdTable = null;
58
+ initPromise = null;
59
+ embedder = null;
60
+ ssdAvailable = true;
61
+ ssdConsecutiveFailures = 0;
62
+ ssdRecoveryProbeTimer = null;
63
+ ssdRecoveryProbeInFlight = false;
64
+ vectorDim;
65
+ /**
66
+ * @param ramDb RAM LanceDB 連接(由 MemoryStore 提供,透過 store.db)
67
+ * @param ssdDb SSD LanceDB 連接(由 MemoryStore 提供,透過 store.ssd)
68
+ *
69
+ * ⚠️ Connections owned by MemoryStore — do not close.
70
+ * ⚠️ GraphStore MUST be constructed AFTER store.ensureInitialized().
71
+ */
72
+ constructor(ramDb, ssdDb, embedder, vectorDim = 1024, ssdRecoveryProbeIntervalMs = DEFAULT_SSD_RECOVERY_PROBE_INTERVAL_MS) {
73
+ this.ssdRecoveryProbeIntervalMs = ssdRecoveryProbeIntervalMs;
74
+ if (!ramDb)
75
+ throw new Error('[GraphStore] ramDb is null/undefined — was MemoryStore initialized before GraphStore?');
76
+ if (!ssdDb)
77
+ throw new Error('[GraphStore] ssdDb is null/undefined — was MemoryStore initialized before GraphStore?');
78
+ this.ramDb = ramDb;
79
+ this.ssdDb = ssdDb;
80
+ if (ramDb === ssdDb)
81
+ this.ssdAvailable = false;
82
+ this.embedder = embedder;
83
+ this.vectorDim = vectorDim;
84
+ }
85
+ handleSsdSuccess() {
86
+ this.ssdConsecutiveFailures = 0;
87
+ }
88
+ handleSsdError(err, operation) {
89
+ if (!this.ssdAvailable)
90
+ return;
91
+ this.ssdConsecutiveFailures++;
92
+ if (this.ssdConsecutiveFailures < SSD_FAILURE_THRESHOLD) {
93
+ console.warn(`[GraphStore] SSD operation failed (${operation}); consecutive failures ${this.ssdConsecutiveFailures}/${SSD_FAILURE_THRESHOLD}: ${err?.message ?? err}`);
94
+ return;
95
+ }
96
+ this.ssdAvailable = false;
97
+ console.log(`[GraphStore] SSD failed ${SSD_FAILURE_THRESHOLD} consecutive times; switching to RAM-Only Mode. Last operation: ${operation}. Error: ${err?.message ?? err}`);
98
+ this.startSsdRecoveryProbe();
99
+ }
100
+ startSsdRecoveryProbe() {
101
+ if (this.ssdRecoveryProbeTimer)
102
+ return;
103
+ this.ssdRecoveryProbeTimer = setInterval(() => {
104
+ void this.probeSsdRecovery();
105
+ }, this.ssdRecoveryProbeIntervalMs);
106
+ this.ssdRecoveryProbeTimer.unref();
107
+ }
108
+ async probeSsdRecovery() {
109
+ if (this.ssdAvailable || this.ssdRecoveryProbeInFlight || !this.ssdTable)
110
+ return;
111
+ this.ssdRecoveryProbeInFlight = true;
112
+ try {
113
+ await this.ssdTable.countRows();
114
+ this.ssdAvailable = true;
115
+ this.handleSsdSuccess();
116
+ this.stopSsdRecoveryProbe();
117
+ console.log("[GraphStore] SSD recovery probe succeeded; leaving RAM-Only Mode");
118
+ }
119
+ catch (err) {
120
+ console.warn(`[GraphStore] SSD recovery probe failed; retries will continue. Error: ${err?.message ?? err}`);
121
+ }
122
+ finally {
123
+ this.ssdRecoveryProbeInFlight = false;
124
+ }
125
+ }
126
+ stopSsdRecoveryProbe() {
127
+ if (!this.ssdRecoveryProbeTimer)
128
+ return;
129
+ clearInterval(this.ssdRecoveryProbeTimer);
130
+ this.ssdRecoveryProbeTimer = null;
131
+ }
132
+ async ensureInitialized() {
133
+ if (this.ramTable)
134
+ return;
135
+ if (this.initPromise)
136
+ return this.initPromise;
137
+ this.initPromise = this.doInitialize().catch((err) => {
138
+ this.initPromise = null;
139
+ throw err;
140
+ });
141
+ return this.initPromise;
142
+ }
143
+ async doInitialize() {
144
+ // Connections already set in constructor (shared from MemoryStore).
145
+ // Directory creation and hydration are MemoryStore's responsibility.
146
+ console.log("[GraphStore] Initializing tables using shared MemoryStore connections...");
147
+ this.ramTable = await this.initTable(this.ramDb, "ram");
148
+ this.ssdTable = await this.initTable(this.ssdDb, "ssd");
149
+ console.log("[GraphStore] Initialization complete");
150
+ }
151
+ async initTable(db, label) {
152
+ const tables = await db.tableNames();
153
+ let table;
154
+ if (tables.includes(GRAPH_TABLE_NAME)) {
155
+ table = await db.openTable(GRAPH_TABLE_NAME);
156
+ console.log(`[GraphStore] [${label}] Opened existing table`);
157
+ }
158
+ else {
159
+ console.log(`[GraphStore] [${label}] Creating new table...`);
160
+ const initialData = [{
161
+ id: "init_00000000000000000000000000000000",
162
+ subject: "_SYSTEM_INIT_",
163
+ relation: "_",
164
+ object: "_",
165
+ sourceMemoryId: "",
166
+ vector: Array(this.vectorDim).fill(0),
167
+ createdAt: 0,
168
+ }];
169
+ table = await db.createTable(GRAPH_TABLE_NAME, initialData);
170
+ console.log(`[GraphStore] [${label}] New table created`);
171
+ }
172
+ // 確保 FTS index(用於 subject/object 關鍵字搜尋)
173
+ try {
174
+ await table.createIndex("subject", {
175
+ config: { type: "fts" },
176
+ replace: true,
177
+ });
178
+ }
179
+ catch (err) {
180
+ if (!err.message?.includes("already exists")) {
181
+ console.warn(`[GraphStore] [${label}] Failed to create FTS index:`, err.message);
182
+ }
183
+ }
184
+ return table;
185
+ }
186
+ /**
187
+ * 將三元組文字 Embedding 化(用於 ANN 搜尋)
188
+ * 拼接方式:subject + relation + object
189
+ */
190
+ async embedTriple(triple) {
191
+ const text = `${triple.subject} ${triple.relation} ${triple.object}`;
192
+ try {
193
+ return await this.embedder.embed(text, 'store');
194
+ }
195
+ catch (err) {
196
+ console.warn(`[GraphStore] embedTriple failed; falling back to zero vector:`, err);
197
+ return Array(this.vectorDim).fill(0);
198
+ }
199
+ }
200
+ /**
201
+ * 寫入單筆三元組
202
+ */
203
+ async addTriple(triple, sourceMemoryId) {
204
+ await this.ensureInitialized();
205
+ const embedding = await this.embedTriple(triple);
206
+ const nowMs = Date.now();
207
+ const entry = {
208
+ id: randomUUID(),
209
+ subject: triple.subject,
210
+ relation: triple.relation,
211
+ object: triple.object,
212
+ sourceMemoryId,
213
+ vector: embedding,
214
+ createdAt: nowMs,
215
+ };
216
+ await this.ramTable.add([entry]);
217
+ await recordAuxTableWrite(this.ramTable, "ram:graph_triples");
218
+ if (this.ssdAvailable) {
219
+ try {
220
+ await this.ssdTable.add([entry]);
221
+ }
222
+ catch (err) {
223
+ this.handleSsdError(err, "addTriple");
224
+ throw err;
225
+ }
226
+ this.handleSsdSuccess();
227
+ await recordAuxTableWrite(this.ssdTable, "ssd:graph_triples");
228
+ }
229
+ return {
230
+ id: entry.id,
231
+ subject: triple.subject,
232
+ relation: triple.relation,
233
+ object: triple.object,
234
+ sourceMemoryId,
235
+ createdAt: nowMs,
236
+ };
237
+ }
238
+ /**
239
+ * 批量寫入三元組
240
+ */
241
+ async addTriples(triples, sourceMemoryId) {
242
+ if (triples.length === 0)
243
+ return [];
244
+ await this.ensureInitialized();
245
+ const nowMs = Date.now();
246
+ const results = [];
247
+ // 批次 embedding(避免逐筆呼叫浪費時間)
248
+ const texts = triples.map(t => `${t.subject} ${t.relation} ${t.object}`);
249
+ let embeddings = [];
250
+ try {
251
+ embeddings = await this.embedder.embedBatch(texts);
252
+ }
253
+ catch (err) {
254
+ console.warn(`[GraphStore] embedBatch failed; falling back to zero vectors:`, err);
255
+ embeddings = texts.map(() => Array(this.vectorDim).fill(0));
256
+ }
257
+ const entries = triples.map((triple, i) => ({
258
+ id: randomUUID(),
259
+ subject: triple.subject,
260
+ relation: triple.relation,
261
+ object: triple.object,
262
+ sourceMemoryId,
263
+ vector: embeddings[i] || Array(this.vectorDim).fill(0),
264
+ createdAt: nowMs,
265
+ }));
266
+ await this.ramTable.add(entries);
267
+ await recordAuxTableWrite(this.ramTable, "ram:graph_triples");
268
+ if (this.ssdAvailable) {
269
+ try {
270
+ await this.ssdTable.add(entries);
271
+ }
272
+ catch (err) {
273
+ this.handleSsdError(err, "addTriples");
274
+ throw err;
275
+ }
276
+ this.handleSsdSuccess();
277
+ await recordAuxTableWrite(this.ssdTable, "ssd:graph_triples");
278
+ }
279
+ for (const entry of entries) {
280
+ results.push({
281
+ id: entry.id,
282
+ subject: entry.subject,
283
+ relation: entry.relation,
284
+ object: entry.object,
285
+ sourceMemoryId: entry.sourceMemoryId,
286
+ createdAt: entry.createdAt,
287
+ });
288
+ }
289
+ return results;
290
+ }
291
+ /**
292
+ * 用 Query 文字找相關實體(ANN 相似度搜尋)
293
+ * @param queryText 查詢文字
294
+ * @param limit 回傳上限
295
+ */
296
+ async findRelatedEntities(queryText, limit = 10) {
297
+ await this.ensureInitialized();
298
+ if (!queryText || queryText.trim() === "")
299
+ return [];
300
+ // 先 embed query
301
+ let queryVector = [];
302
+ try {
303
+ queryVector = await this.embedder.embed(queryText);
304
+ }
305
+ catch (err) {
306
+ console.warn(`[GraphStore] findRelatedEntities embedding failed:`, err);
307
+ return [];
308
+ }
309
+ if (!queryVector || queryVector.length === 0)
310
+ return [];
311
+ try {
312
+ const results = await this.ramTable.search(queryVector).limit(limit).toArray();
313
+ return results
314
+ .filter((row) => !row.id.startsWith("init_"))
315
+ .map((row) => ({
316
+ id: row.id,
317
+ subject: row.subject,
318
+ relation: row.relation,
319
+ object: row.object,
320
+ sourceMemoryId: row.sourceMemoryId,
321
+ createdAt: Number(row.createdAt) || Date.now(),
322
+ }));
323
+ }
324
+ catch (err) {
325
+ console.warn(`[GraphStore] findRelatedEntities search failed:`, err.message);
326
+ return [];
327
+ }
328
+ }
329
+ /**
330
+ * 找某實體的所有三元組(subject 匹配)
331
+ */
332
+ async findTriplesBySubject(subject, limit = 100) {
333
+ await this.ensureInitialized();
334
+ if (!subject || subject.trim() === "")
335
+ return [];
336
+ try {
337
+ // subject 精確匹配
338
+ const results = await this.ramTable
339
+ .query()
340
+ .where(`subject = '${subject.replace(/'/g, "''")}'`)
341
+ .limit(limit)
342
+ .toArray();
343
+ return results
344
+ .filter((row) => !row.id.startsWith("init_"))
345
+ .map((row) => ({
346
+ id: row.id,
347
+ subject: row.subject,
348
+ relation: row.relation,
349
+ object: row.object,
350
+ sourceMemoryId: row.sourceMemoryId,
351
+ createdAt: Number(row.createdAt) || Date.now(),
352
+ }));
353
+ }
354
+ catch (err) {
355
+ console.warn(`[GraphStore] findTriplesBySubject failed:`, err.message);
356
+ return [];
357
+ }
358
+ }
359
+ /**
360
+ * 找某實體的所有三元組(object 匹配)
361
+ */
362
+ async findTriplesByObject(object, limit = 1000) {
363
+ await this.ensureInitialized();
364
+ if (!object || object.trim() === "")
365
+ return [];
366
+ try {
367
+ const results = await this.ramTable
368
+ .query()
369
+ .where(`object = '${object.replace(/'/g, "''")}'`)
370
+ .limit(limit)
371
+ .toArray();
372
+ return results
373
+ .filter((row) => !row.id.startsWith("init_"))
374
+ .map((row) => ({
375
+ id: row.id,
376
+ subject: row.subject,
377
+ relation: row.relation,
378
+ object: row.object,
379
+ sourceMemoryId: row.sourceMemoryId,
380
+ createdAt: Number(row.createdAt) || Date.now(),
381
+ }));
382
+ }
383
+ catch (err) {
384
+ console.warn(`[GraphStore] findTriplesByObject failed:`, err.message);
385
+ return [];
386
+ }
387
+ }
388
+ async findTriplesByEntity(entity, direction = 'both', limit = 1000) {
389
+ if (direction === 'out')
390
+ return this.findTriplesBySubject(entity, limit);
391
+ if (direction === 'in')
392
+ return this.findTriplesByObject(entity, limit);
393
+ const byId = new Map();
394
+ for (const triple of await this.findTriplesBySubject(entity, limit)) {
395
+ byId.set(triple.id, triple);
396
+ }
397
+ for (const triple of await this.findTriplesByObject(entity, limit)) {
398
+ byId.set(triple.id, triple);
399
+ }
400
+ return Array.from(byId.values()).slice(0, limit);
401
+ }
402
+ /**
403
+ * 查詢與某記憶 ID 關聯的所有三元組
404
+ */
405
+ async findTriplesByMemoryId(memoryId) {
406
+ await this.ensureInitialized();
407
+ if (!memoryId)
408
+ return [];
409
+ try {
410
+ const results = await this.ramTable
411
+ .query()
412
+ .where(`\`sourceMemoryId\` = '${memoryId.replace(/'/g, "''")}'`)
413
+ .limit(100)
414
+ .toArray();
415
+ return results
416
+ .filter((row) => !row.id.startsWith("init_"))
417
+ .map((row) => ({
418
+ id: row.id,
419
+ subject: row.subject,
420
+ relation: row.relation,
421
+ object: row.object,
422
+ sourceMemoryId: row.sourceMemoryId,
423
+ createdAt: Number(row.createdAt) || Date.now(),
424
+ }));
425
+ }
426
+ catch (err) {
427
+ console.warn(`[GraphStore] findTriplesByMemoryId failed:`, err.message);
428
+ return [];
429
+ }
430
+ }
431
+ /**
432
+ * 圖譜語意擴展:給定 Query,回傳 subject/object 關鍵詞列表
433
+ * 用於 Hook trigger 時擴展 keyword matching 範圍
434
+ *
435
+ * @param queryText 原始 query
436
+ * @param limit 回傳數量上限
437
+ * @returns 擴展後的關鍵詞列表
438
+ */
439
+ async expandQueryKeywords(queryText, limit = 5) {
440
+ const related = await this.findRelatedEntities(queryText, limit * 2);
441
+ if (related.length === 0)
442
+ return [];
443
+ const keywords = new Set();
444
+ for (const triple of related) {
445
+ // 將 subject/object 加入 keyword set(去除重複)
446
+ if (triple.subject && triple.subject.length >= 2) {
447
+ keywords.add(triple.subject);
448
+ }
449
+ if (triple.object && triple.object.length >= 2) {
450
+ keywords.add(triple.object);
451
+ }
452
+ }
453
+ return Array.from(keywords).slice(0, limit);
454
+ }
455
+ /**
456
+ * 圖譜語意擴展(完整版):回傳相關三元組 + 擴展後關鍵詞
457
+ * 供 Hook trigger 時使用
458
+ */
459
+ async semanticExpand(queryText, limit = 10) {
460
+ const triples = await this.findRelatedEntities(queryText, limit);
461
+ const keywords = await this.expandQueryKeywords(queryText, limit);
462
+ return { triples, expandedKeywords: keywords };
463
+ }
464
+ /** 取得目前圖譜大小(除錯用) */
465
+ async count() {
466
+ await this.ensureInitialized();
467
+ const total = await this.ramTable.countRows();
468
+ // 扣掉 init_ 系統列
469
+ return Math.max(0, total - 1);
470
+ }
471
+ /** Graceful shutdown — connections owned by MemoryStore, do not close here */
472
+ async shutdown() {
473
+ console.log("[GraphStore] Shutdown complete (connections remain managed by MemoryStore)");
474
+ this.stopSsdRecoveryProbe();
475
+ this.ramTable = null;
476
+ this.ssdTable = null;
477
+ }
478
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * StatusManager — 記憶狀態單一所有人(P0-3)
3
+ *
4
+ * 所有 status 變更(active / superseded / deprecated / archived / trashed)必須經過此模組。
5
+ * 內部同時更新 metadata.status + row.status(雙欄位同步),並寫入 audit log。
6
+ *
7
+ * 設計原則:
8
+ * - 純 class,只依賴 MemoryStore
9
+ * - changeStatus() 走 store.update()(自動進 WAL exactly-once 保護)
10
+ * - 所有成功/失敗都記 audit row
11
+ * - changeStatusBatch() 逐筆 try/catch,單筆失敗不中斷整 batch
12
+ */
13
+ import { MemoryStore } from "./store-v4.js";
14
+ import type { StatusChangeRequest, StatusChangeResult, StatusAuditRow } from "../types.js";
15
+ export declare class StatusManager {
16
+ private store;
17
+ constructor(store: MemoryStore);
18
+ changeStatus(req: StatusChangeRequest): Promise<StatusChangeResult>;
19
+ /**
20
+ * 逐筆呼叫 changeStatus(),單筆失敗不中斷整 batch。
21
+ * 每 20 筆輸出 progress log,避免凌晨 batch 卡住時無感。
22
+ */
23
+ changeStatusBatch(reqs: StatusChangeRequest[]): Promise<StatusChangeResult[]>;
24
+ queryAuditLog(opts: {
25
+ memoryId?: string;
26
+ since?: number;
27
+ source?: string;
28
+ limit?: number;
29
+ }): Promise<StatusAuditRow[]>;
30
+ /**
31
+ * 新記憶建立時,row.status 與 metadata.status 已在 store.store() 一次寫入;
32
+ * 這裡只補 audit log,避免再次觸發 LanceDB update metadata parser。
33
+ */
34
+ recordCreation(req: {
35
+ memoryId: string;
36
+ source: string;
37
+ meta?: Record<string, unknown>;
38
+ }): Promise<string>;
39
+ /**
40
+ * 安全寫入 audit log — 失敗只 warn,不影響主流程
41
+ */
42
+ private safeRecordAudit;
43
+ private parseMetadata;
44
+ }