rag-memory-epf-mcp 6.3.1 → 6.3.2
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/README.md +24 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +387 -144
- package/dist/src/backfillCoordinator.d.ts +1 -0
- package/dist/src/backfillCoordinator.js +23 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,6 +154,30 @@ storeDocument(id, content, metadata)
|
|
|
154
154
|
|
|
155
155
|
## Changelog
|
|
156
156
|
|
|
157
|
+
### v6.3.2 (2026-09-25)
|
|
158
|
+
|
|
159
|
+
- **Changed — on macOS the engine takes `-wal` and `-shm` out of cloud sync.** It sets
|
|
160
|
+
`com.apple.fileprovider.ignore#P = 1` on both files after every open (SQLite recreates them after the
|
|
161
|
+
last connection closes, so once is not enough). Measured with Google Drive for desktop: the client
|
|
162
|
+
treated `-wal` as a synced item and put a new file at the path while the engine held the old one open
|
|
163
|
+
(`lsof -p` showed the descriptor inside `FileProvider/…/wharf/delete/`); from then on every write
|
|
164
|
+
failed with `SQLITE_IOERR` ("disk I/O error") until the engine restarted — 10 s to 3 min after start,
|
|
165
|
+
with one engine attached. With the attribute set, the same live database kept the same `-wal` for
|
|
166
|
+
25 min of use. The main database file still syncs. Other platforms: no-op.
|
|
167
|
+
- **Changed — a swapped sidecar is detected and the connection reopened.** The idle tick compares the
|
|
168
|
+
`-wal`/`-shm` inodes with the ones recorded at open (two consecutive mismatches) and reopens with the
|
|
169
|
+
same configuration; an `SQLITE_IOERR*` from a checkpoint or a tool call reopens too. A tool that hit
|
|
170
|
+
the I/O error is retried once only if repeating it changes nothing (reads, `createEntities`,
|
|
171
|
+
`createRelations`, `addObservations`, `deleteEntities`, `deleteRelations`); other tools get the
|
|
172
|
+
error back after the reopen. A reopen waits for running tool calls, is not done while shutting
|
|
173
|
+
down, and is braked after 3 in 10 s. A failed reopen retries with backoff and, after 10 failures,
|
|
174
|
+
shuts the server down so the host shows it as failed. A reconciliation cut by a reopen reruns.
|
|
175
|
+
- Known limit: writes acknowledged in the last ~1 s before a swap can be lost if the old file is no
|
|
176
|
+
longer readable (the engine tries one checkpoint on the old handle first and logs `walsz-before=`).
|
|
177
|
+
- The 6.3.1 `wal_checkpoint(TRUNCATE)` is unchanged. `RESTART` (± `journal_size_limit`) was measured and
|
|
178
|
+
rejected: after SIGKILL its WAL replayed onto a foreign main file 3/3 times.
|
|
179
|
+
- No tool, argument, return shape or schema changed. Spec: `specs/changes/wal-sidecar-guard/`.
|
|
180
|
+
|
|
157
181
|
### v6.3.1
|
|
158
182
|
|
|
159
183
|
- **Changed — the engine empties the WAL while it is idle.** It runs `wal_checkpoint(TRUNCATE)` itself:
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ interface DetailedContext {
|
|
|
51
51
|
export declare class SyncCasConflictError extends Error {
|
|
52
52
|
constructor(documentId: string);
|
|
53
53
|
}
|
|
54
|
+
export declare function isIoError(e: unknown): boolean;
|
|
54
55
|
export declare function setSyncFaultPoint(point: string | null, fn: (() => void) | null): void;
|
|
55
56
|
export interface ReuseCandidate {
|
|
56
57
|
rowid: number;
|
|
@@ -82,6 +83,7 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
82
83
|
gate?: EmbeddingGate;
|
|
83
84
|
__testForceFkOff?: boolean;
|
|
84
85
|
}): Promise<void>;
|
|
86
|
+
private openConnection;
|
|
85
87
|
private ensureCurrentProfile;
|
|
86
88
|
private buildRealLoader;
|
|
87
89
|
startReconciliation(): Promise<void>;
|
|
@@ -102,6 +104,26 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
102
104
|
}>;
|
|
103
105
|
}>;
|
|
104
106
|
checkpointWal(): boolean;
|
|
107
|
+
walIno: bigint | null;
|
|
108
|
+
shmIno: bigint | null;
|
|
109
|
+
reopenCount: number;
|
|
110
|
+
private reopenTimes;
|
|
111
|
+
private inFlight;
|
|
112
|
+
private pendingReopen;
|
|
113
|
+
private walMismatchTicks;
|
|
114
|
+
private walWatchPausedUntil;
|
|
115
|
+
private walWatchOff;
|
|
116
|
+
private lastWalSize;
|
|
117
|
+
private dbDown;
|
|
118
|
+
shuttingDown: boolean;
|
|
119
|
+
onFatal: (() => void) | null;
|
|
120
|
+
guardSidecars(): void;
|
|
121
|
+
walTick(): boolean;
|
|
122
|
+
private stormCount;
|
|
123
|
+
reopenDb(reason: string, force?: boolean): boolean;
|
|
124
|
+
private openAfterClose;
|
|
125
|
+
private retryOpen;
|
|
126
|
+
runWithIoRecovery<T>(tool: string, fn: () => Promise<T>): Promise<T>;
|
|
105
127
|
private walTimer;
|
|
106
128
|
private walSoon;
|
|
107
129
|
startWalKeeper(tickMs?: number): void;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import Database from 'better-sqlite3';
|
|
|
6
6
|
import * as sqliteVec from 'sqlite-vec';
|
|
7
7
|
import { get_encoding } from 'tiktoken';
|
|
8
8
|
import fsSync from 'fs';
|
|
9
|
+
import { execFileSync } from 'child_process';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
11
12
|
import { pipeline, env } from '@huggingface/transformers';
|
|
@@ -136,6 +137,30 @@ const TEXT_BUILDER_VERSION = 'tb1';
|
|
|
136
137
|
export class SyncCasConflictError extends Error {
|
|
137
138
|
constructor(documentId) { super(`sync CAS conflict on ${documentId}`); this.name = 'SyncCasConflictError'; }
|
|
138
139
|
}
|
|
140
|
+
// Sidecar guard (specs/changes/wal-sidecar-guard). macOS File Provider attribute that takes an
|
|
141
|
+
// item out of sync; Google Drive for desktop honours it (measured: isExcludedFromSync = 1).
|
|
142
|
+
const SYNC_EXCLUDE_ATTR = 'com.apple.fileprovider.ignore#P';
|
|
143
|
+
const REOPEN_STORM_WINDOW_MS = 10_000;
|
|
144
|
+
const REOPEN_STORM_MAX = 3;
|
|
145
|
+
const REOPEN_MAX_TRIES = 10;
|
|
146
|
+
const WAL_WATCH_PAUSE_MS = 60_000;
|
|
147
|
+
// Tools that may be retried once after an I/O error reopened the connection: reads, and writes
|
|
148
|
+
// whose repetition changes nothing (INSERT OR IGNORE / content dedup / deletes). Everything else
|
|
149
|
+
// gets the error back so the caller decides — e.g. syncDocumentFromFile spans several
|
|
150
|
+
// transactions and its repeat-safety is not proven.
|
|
151
|
+
const IDEMPOTENT_TOOLS = new Set([
|
|
152
|
+
'readGraph', 'searchNodes', 'openNodes', 'getNeighbors', 'hybridSearch', 'getDetailedContext',
|
|
153
|
+
'getKnowledgeGraphStats', 'listDocuments', 'exportGraph', 'getMigrationStatus', 'getGraphMetrics',
|
|
154
|
+
'detectCommunities', 'analyzeGraphStructure', 'getObservationHistory',
|
|
155
|
+
'createEntities', 'createRelations', 'addObservations',
|
|
156
|
+
// deleteObservations / deleteDocuments are left out on purpose: a retry after a committed
|
|
157
|
+
// delete reports 0 deleted, which a caller reads as "nothing was deleted".
|
|
158
|
+
'deleteEntities', 'deleteRelations',
|
|
159
|
+
]);
|
|
160
|
+
export function isIoError(e) {
|
|
161
|
+
const code = e?.code;
|
|
162
|
+
return typeof code === 'string' && code.startsWith('SQLITE_IOERR');
|
|
163
|
+
}
|
|
139
164
|
// Test-only fault hook (v13 setMigrationFaultPoint 선례 — 환경변수 금지: 상시 스위치는
|
|
140
165
|
// 오설정 한 줄로 sync 를 깬다).
|
|
141
166
|
let __syncFaultHook = null;
|
|
@@ -219,38 +244,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
219
244
|
// 호출부에서 읽히게 하기 위해서다.
|
|
220
245
|
async initialize(opts = {}) {
|
|
221
246
|
console.error('🚀 Initializing RAG Knowledge Graph MCP Server...');
|
|
222
|
-
this.
|
|
223
|
-
sqliteVec.load(this.db);
|
|
224
|
-
this.db.pragma('journal_mode = WAL');
|
|
225
|
-
this.db.pragma('synchronous = NORMAL');
|
|
226
|
-
this.db.pragma('busy_timeout = 5000');
|
|
227
|
-
this.db.pragma('cache_size = -32000');
|
|
228
|
-
this.db.pragma('temp_store = MEMORY');
|
|
229
|
-
// Opt-in mmap (see MMAP_SIZE above). Read the applied value back so the banner reports
|
|
230
|
-
// what SQLite did, not what we asked for (a compile-time cap can lower it silently).
|
|
231
|
-
this.db.pragma(`mmap_size = ${MMAP_SIZE}`);
|
|
232
|
-
this.mmapApplied = Number(this.db.pragma('mmap_size', { simple: true }));
|
|
233
|
-
this.db.pragma('foreign_keys = ON');
|
|
234
|
-
// spec §5.2: 관찰 lifecycle 의 무결성은 전부 FK CASCADE 를 전제한다 — root 를 지우면
|
|
235
|
-
// revision 이, revision 을 지우면 source 가 따라가야 history 가 고아로 남지 않는다.
|
|
236
|
-
// FK 가 꺼진 채 돌면 그 계약이 조용히 무효가 되고, 그게 최악이다. 스키마를 건드리기
|
|
237
|
-
// 전에(= runMigrations 앞에서) 멈춘다.
|
|
238
|
-
// 트랜잭션 내부에서는 이 pragma 가 no-op 이므로(실측 before=1·during=1·after=1)
|
|
239
|
-
// 부팅 시점 확인이 유일한 방어 지점이다.
|
|
240
|
-
//
|
|
241
|
-
// 음성 대조군 주입은 **인자로만** 받는다. 환경변수로 두면 프로덕션 경로에
|
|
242
|
-
// "부팅을 막는 스위치"가 상시 존재하게 되고, 오설정 한 줄로 서버가 안 뜬다
|
|
243
|
-
// (advisor beta 자기의심 2 = "더 나쁘다"). 테스트는 manager 를 직접 만들므로
|
|
244
|
-
// 인자 주입으로 충분하다.
|
|
245
|
-
if (opts.__testForceFkOff)
|
|
246
|
-
this.db.pragma('foreign_keys = OFF');
|
|
247
|
-
{
|
|
248
|
-
const fk = this.db.pragma('foreign_keys', { simple: true });
|
|
249
|
-
if (Number(fk) !== 1) {
|
|
250
|
-
throw new Error(`foreign_keys is ${fk}, expected 1. The observation lifecycle relies on FK CASCADE ` +
|
|
251
|
-
`for history integrity; refusing to run migrations without it.`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
247
|
+
this.openConnection(opts.__testForceFkOff === true);
|
|
254
248
|
this.encoding = get_encoding("cl100k_base");
|
|
255
249
|
await this.runMigrations();
|
|
256
250
|
this.currentProfileId = this.ensureCurrentProfile();
|
|
@@ -259,6 +253,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
259
253
|
this.db.prepare(`INSERT INTO server_meta (key, value) VALUES ('current_default_chunker', ?)
|
|
260
254
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
261
255
|
.run(effectiveSignature(DEFAULT_MAX_TOKENS));
|
|
256
|
+
// The writes above created the sidecars; mark them before a sync client can take them.
|
|
257
|
+
this.guardSidecars();
|
|
262
258
|
this.embeddingsMode = opts.skipModel
|
|
263
259
|
? 'off'
|
|
264
260
|
: (process.env.RAG_MEMORY_EMBEDDINGS || 'lazy');
|
|
@@ -287,6 +283,42 @@ export class RAGKnowledgeGraphManager {
|
|
|
287
283
|
const systemInfo = getSystemInfo();
|
|
288
284
|
console.error(`📊 System Info: ${systemInfo.toolCounts.total} tools available (${systemInfo.toolCounts.knowledgeGraph} knowledge graph, ${systemInfo.toolCounts.rag} RAG, ${systemInfo.toolCounts.graphQuery} query)`);
|
|
289
285
|
}
|
|
286
|
+
// Opens the connection and applies everything the engine attaches to it. Shared by
|
|
287
|
+
// initialize() and reopenDb() so a reopened connection is configured exactly like the first.
|
|
288
|
+
openConnection(forceFkOff = false) {
|
|
289
|
+
this.db = new Database(DB_FILE_PATH);
|
|
290
|
+
sqliteVec.load(this.db);
|
|
291
|
+
this.db.pragma('journal_mode = WAL');
|
|
292
|
+
this.db.pragma('synchronous = NORMAL');
|
|
293
|
+
this.db.pragma('busy_timeout = 5000');
|
|
294
|
+
this.db.pragma('cache_size = -32000');
|
|
295
|
+
this.db.pragma('temp_store = MEMORY');
|
|
296
|
+
// Opt-in mmap (see MMAP_SIZE above). Read the applied value back so the banner reports
|
|
297
|
+
// what SQLite did, not what we asked for (a compile-time cap can lower it silently).
|
|
298
|
+
this.db.pragma(`mmap_size = ${MMAP_SIZE}`);
|
|
299
|
+
this.mmapApplied = Number(this.db.pragma('mmap_size', { simple: true }));
|
|
300
|
+
this.db.pragma('foreign_keys = ON');
|
|
301
|
+
// spec §5.2: 관찰 lifecycle 의 무결성은 전부 FK CASCADE 를 전제한다 — root 를 지우면
|
|
302
|
+
// revision 이, revision 을 지우면 source 가 따라가야 history 가 고아로 남지 않는다.
|
|
303
|
+
// FK 가 꺼진 채 돌면 그 계약이 조용히 무효가 되고, 그게 최악이다. 스키마를 건드리기
|
|
304
|
+
// 전에(= runMigrations 앞에서) 멈춘다.
|
|
305
|
+
// 트랜잭션 내부에서는 이 pragma 가 no-op 이므로(실측 before=1·during=1·after=1)
|
|
306
|
+
// 부팅 시점 확인이 유일한 방어 지점이다.
|
|
307
|
+
//
|
|
308
|
+
// 음성 대조군 주입은 **인자로만** 받는다. 환경변수로 두면 프로덕션 경로에
|
|
309
|
+
// "부팅을 막는 스위치"가 상시 존재하게 되고, 오설정 한 줄로 서버가 안 뜬다
|
|
310
|
+
// (advisor beta 자기의심 2 = "더 나쁘다"). 테스트는 manager 를 직접 만들므로
|
|
311
|
+
// 인자 주입으로 충분하다.
|
|
312
|
+
if (forceFkOff)
|
|
313
|
+
this.db.pragma('foreign_keys = OFF');
|
|
314
|
+
{
|
|
315
|
+
const fk = this.db.pragma('foreign_keys', { simple: true });
|
|
316
|
+
if (Number(fk) !== 1) {
|
|
317
|
+
throw new Error(`foreign_keys is ${fk}, expected 1. The observation lifecycle relies on FK CASCADE ` +
|
|
318
|
+
`for history integrity; refusing to run migrations without it.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
290
322
|
// Upsert the stored-vector compatibility profile (spec §6c layer 2) and record
|
|
291
323
|
// retrieval config in server_meta (layer 3 — never a backfill trigger).
|
|
292
324
|
ensureCurrentProfile() {
|
|
@@ -544,6 +576,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
544
576
|
// session construction at exit — risks an ugly abort message, never data
|
|
545
577
|
// loss. Hanging forever is the alternative and is worse.
|
|
546
578
|
async shutdownAll() {
|
|
579
|
+
this.shuttingDown = true;
|
|
547
580
|
console.error('\n🧹 Cleaning up...');
|
|
548
581
|
try {
|
|
549
582
|
await this.coordinator?.shutdown(5000);
|
|
@@ -626,10 +659,206 @@ export class RAGKnowledgeGraphManager {
|
|
|
626
659
|
this.db.pragma('busy_timeout = 5000');
|
|
627
660
|
}
|
|
628
661
|
}
|
|
629
|
-
catch {
|
|
662
|
+
catch (e) {
|
|
663
|
+
// An I/O error here means the connection is pointing at a file that is no longer the one
|
|
664
|
+
// at the path (see guardSidecars). Retrying on the same connection cannot succeed.
|
|
665
|
+
if (isIoError(e))
|
|
666
|
+
this.reopenDb(`checkpoint-${e.code}`, true);
|
|
630
667
|
return false; // inside a transaction, or the file is locked — the next tick retries
|
|
631
668
|
}
|
|
632
669
|
}
|
|
670
|
+
// --- Sidecar guard (specs/changes/wal-sidecar-guard) -------------------------------------
|
|
671
|
+
// Measured 2026-09-25 (macOS + Google Drive): the sync client treated `-wal` as a synced item
|
|
672
|
+
// and swapped a new file in at the path while the engine held the old one open; every write
|
|
673
|
+
// then failed with SQLITE_IOERR until restart. Excluding the sidecars from sync takes them out
|
|
674
|
+
// of the client's hands, and it matches the WAL-exit contract: a WAL that never leaves this
|
|
675
|
+
// machine cannot be replayed onto another machine's main file. The attribute lives on the
|
|
676
|
+
// inode and SQLite recreates both files after the last connection closes, so it is set after
|
|
677
|
+
// every open and whenever a sidecar at the path is not the one recorded here.
|
|
678
|
+
walIno = null;
|
|
679
|
+
shmIno = null;
|
|
680
|
+
reopenCount = 0;
|
|
681
|
+
reopenTimes = [];
|
|
682
|
+
inFlight = 0; // tool calls running (reopen waits for them)
|
|
683
|
+
pendingReopen = null;
|
|
684
|
+
walMismatchTicks = 0; // a swap must be seen on 2 consecutive ticks
|
|
685
|
+
walWatchPausedUntil = 0; // storm brake for the inode watch
|
|
686
|
+
walWatchOff = false; // file system gives no inode (ino 0)
|
|
687
|
+
lastWalSize = 0;
|
|
688
|
+
dbDown = null;
|
|
689
|
+
shuttingDown = false;
|
|
690
|
+
// Set by main(): the full shutdown (closes the MCP transport first, then the engine). Without it
|
|
691
|
+
// the process would stay alive on stdin with no database.
|
|
692
|
+
onFatal = null;
|
|
693
|
+
guardSidecars() {
|
|
694
|
+
const wal = DB_FILE_PATH + '-wal', shm = DB_FILE_PATH + '-shm';
|
|
695
|
+
if (process.platform === 'darwin') {
|
|
696
|
+
for (const p of [wal, shm]) {
|
|
697
|
+
if (!fsSync.existsSync(p))
|
|
698
|
+
continue;
|
|
699
|
+
try {
|
|
700
|
+
execFileSync('/usr/bin/xattr', ['-w', SYNC_EXCLUDE_ATTR, '1', p], { stdio: 'ignore' });
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
// Fail open: the attribute is protection, not a correctness condition.
|
|
704
|
+
console.error(`⚠️ sidecar sync-exclude failed: ${p}: ${e.message}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const w = fsSync.statSync(wal, { throwIfNoEntry: false, bigint: true });
|
|
709
|
+
const h = fsSync.statSync(shm, { throwIfNoEntry: false, bigint: true });
|
|
710
|
+
this.walIno = w ? w.ino : null;
|
|
711
|
+
this.shmIno = h ? h.ino : null;
|
|
712
|
+
if (w && w.ino === 0n && !this.walWatchOff) {
|
|
713
|
+
this.walWatchOff = true;
|
|
714
|
+
console.error('⚠️ file system reports inode 0 — sidecar swap watch disabled');
|
|
715
|
+
}
|
|
716
|
+
this.walMismatchTicks = 0;
|
|
717
|
+
}
|
|
718
|
+
// Runs on the WAL keeper tick. Returns true when it reopened.
|
|
719
|
+
walTick() {
|
|
720
|
+
if (this.shuttingDown)
|
|
721
|
+
return false;
|
|
722
|
+
if (!this.db)
|
|
723
|
+
return this.dbDown ? this.retryOpen() : false;
|
|
724
|
+
if (this.walWatchOff || Date.now() < this.walWatchPausedUntil)
|
|
725
|
+
return false;
|
|
726
|
+
const w = fsSync.statSync(DB_FILE_PATH + '-wal', { throwIfNoEntry: false, bigint: true });
|
|
727
|
+
const h = fsSync.statSync(DB_FILE_PATH + '-shm', { throwIfNoEntry: false, bigint: true });
|
|
728
|
+
if (this.walIno === null) {
|
|
729
|
+
// Nothing recorded yet (no write had created the file): record and mark it now.
|
|
730
|
+
if (w)
|
|
731
|
+
this.guardSidecars();
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
const walOk = !!w && w.ino === this.walIno;
|
|
735
|
+
const shmOk = this.shmIno === null || (!!h && h.ino === this.shmIno);
|
|
736
|
+
if (walOk && shmOk) {
|
|
737
|
+
this.walMismatchTicks = 0;
|
|
738
|
+
if (w)
|
|
739
|
+
this.lastWalSize = Number(w.size);
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
// Two consecutive ticks, so a value that flickers back within a second is not a swap.
|
|
743
|
+
if (++this.walMismatchTicks < 2)
|
|
744
|
+
return false;
|
|
745
|
+
const reason = !w ? 'wal-missing' : !walOk ? 'wal-inode-changed' : 'shm-inode-changed';
|
|
746
|
+
if (this.inFlight > 0) {
|
|
747
|
+
this.pendingReopen = reason;
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
return this.reopenDb(reason);
|
|
751
|
+
}
|
|
752
|
+
stormCount() {
|
|
753
|
+
const now = Date.now();
|
|
754
|
+
return this.reopenTimes.filter(t => now - t < REOPEN_STORM_WINDOW_MS).length;
|
|
755
|
+
}
|
|
756
|
+
// Close and reopen the connection with the same configuration. Migrations are not rerun.
|
|
757
|
+
// Returns false when there was nothing to reopen or the reopen failed (the engine then
|
|
758
|
+
// retries with backoff from the keeper tick and, after REOPEN_MAX_TRIES, exits so the host
|
|
759
|
+
// sees a failed server instead of a live one that answers every call with an error).
|
|
760
|
+
// force = an I/O error was seen on this handle: reopen even inside the storm window (holding a
|
|
761
|
+
// dead handle helps nobody). The storm brake is for the inode watch.
|
|
762
|
+
reopenDb(reason, force = false) {
|
|
763
|
+
if (this.shuttingDown || !this.db)
|
|
764
|
+
return false;
|
|
765
|
+
if (!force && this.stormCount() >= REOPEN_STORM_MAX) {
|
|
766
|
+
// Keep the connection; pause the inode watch so a flickering file system cannot make
|
|
767
|
+
// this a once-a-second close/open loop. An IOERR path still gets its error.
|
|
768
|
+
this.walWatchPausedUntil = Date.now() + WAL_WATCH_PAUSE_MS;
|
|
769
|
+
console.error(`⚠️ reopen storm (${REOPEN_STORM_MAX} in ${REOPEN_STORM_WINDOW_MS / 1000}s) — watch paused, reason=${reason}`);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
const old = this.db;
|
|
773
|
+
// Salvage: frames committed to the old WAL after the last checkpoint (≤1 s) go to the main
|
|
774
|
+
// file if the old descriptor is still readable. An I/O error here just means they are gone.
|
|
775
|
+
// busy_timeout 0: another engine's read snapshot must not stall this for 5 s.
|
|
776
|
+
try {
|
|
777
|
+
old.pragma('busy_timeout = 0');
|
|
778
|
+
old.pragma('wal_checkpoint(TRUNCATE)');
|
|
779
|
+
}
|
|
780
|
+
catch { /* old file unreadable */ }
|
|
781
|
+
try {
|
|
782
|
+
old.close();
|
|
783
|
+
}
|
|
784
|
+
catch { /* the handle may already be unusable */ }
|
|
785
|
+
this.db = null;
|
|
786
|
+
this.reopenTimes.push(Date.now());
|
|
787
|
+
return this.openAfterClose(reason);
|
|
788
|
+
}
|
|
789
|
+
openAfterClose(reason) {
|
|
790
|
+
try {
|
|
791
|
+
this.openConnection();
|
|
792
|
+
this.guardSidecars();
|
|
793
|
+
}
|
|
794
|
+
catch (e) {
|
|
795
|
+
try {
|
|
796
|
+
this.db?.close();
|
|
797
|
+
}
|
|
798
|
+
catch { /* half-open */ }
|
|
799
|
+
this.db = null;
|
|
800
|
+
const tries = (this.dbDown?.tries ?? 0) + 1;
|
|
801
|
+
const delay = Math.min(1000 * 2 ** (tries - 1), 60_000);
|
|
802
|
+
this.dbDown = { reason, tries, nextAt: Date.now() + delay };
|
|
803
|
+
console.error(`❌ db reopen failed (reason=${reason}, try=${tries}): ${e.message}`);
|
|
804
|
+
if (tries >= REOPEN_MAX_TRIES) {
|
|
805
|
+
console.error('❌ database cannot be reopened — exiting so the host shows a failed server');
|
|
806
|
+
process.exitCode = 1;
|
|
807
|
+
if (this.onFatal)
|
|
808
|
+
this.onFatal();
|
|
809
|
+
else
|
|
810
|
+
void this.shutdownAll().finally(() => process.exit(process.exitCode ?? 1));
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
this.dbDown = null;
|
|
815
|
+
this.pendingReopen = null;
|
|
816
|
+
this.reopenCount++;
|
|
817
|
+
console.error(`🔁 db reopened (reason=${reason}, n=${this.reopenCount}, walsz-before=${this.lastWalSize})`);
|
|
818
|
+
// A reconciliation cut short by the old handle is 'pending', not failed — run it again.
|
|
819
|
+
if (this.coordinator?.reconState === 'pending')
|
|
820
|
+
void this.startReconciliation().catch(() => { });
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
retryOpen() {
|
|
824
|
+
if (this.shuttingDown)
|
|
825
|
+
return false;
|
|
826
|
+
if (!this.dbDown || Date.now() < this.dbDown.nextAt)
|
|
827
|
+
return false;
|
|
828
|
+
return this.openAfterClose(`retry-${this.dbDown.reason}`);
|
|
829
|
+
}
|
|
830
|
+
// Tool-call wrapper: an SQLITE_IOERR* reopens the connection. The call is retried once only
|
|
831
|
+
// when the tool is idempotent — a failed transaction is rolled back whole, but a tool can
|
|
832
|
+
// span several transactions, so a non-idempotent one gets the error back (after the reopen)
|
|
833
|
+
// and the caller decides. Too many reopens in a short window → no retry, surface the error.
|
|
834
|
+
async runWithIoRecovery(tool, fn) {
|
|
835
|
+
if (!this.db && this.dbDown)
|
|
836
|
+
this.retryOpen();
|
|
837
|
+
const handle = this.db;
|
|
838
|
+
this.inFlight++;
|
|
839
|
+
try {
|
|
840
|
+
try {
|
|
841
|
+
return await fn();
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
if (!isIoError(e))
|
|
845
|
+
throw e;
|
|
846
|
+
const storm = this.stormCount() >= REOPEN_STORM_MAX;
|
|
847
|
+
// Several calls hit by one swap reopen once: if another call already replaced the
|
|
848
|
+
// handle, just use the new one.
|
|
849
|
+
const reopened = (this.db !== null && this.db !== handle)
|
|
850
|
+
|| this.reopenDb(`tool-${tool}-${e.code}`, true);
|
|
851
|
+
if (storm || !reopened || !IDEMPOTENT_TOOLS.has(tool))
|
|
852
|
+
throw e;
|
|
853
|
+
return await fn();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
finally {
|
|
857
|
+
this.inFlight--;
|
|
858
|
+
if (this.inFlight === 0 && this.pendingReopen)
|
|
859
|
+
this.reopenDb(this.pendingReopen);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
633
862
|
walTimer = null;
|
|
634
863
|
walSoon = null;
|
|
635
864
|
// Every write path ends up here without having to be listed: the periodic tick looks at
|
|
@@ -638,7 +867,15 @@ export class RAGKnowledgeGraphManager {
|
|
|
638
867
|
startWalKeeper(tickMs = 1000) {
|
|
639
868
|
if (this.walTimer)
|
|
640
869
|
return;
|
|
641
|
-
this.walTimer = setInterval(() => {
|
|
870
|
+
this.walTimer = setInterval(() => {
|
|
871
|
+
try {
|
|
872
|
+
this.walTick();
|
|
873
|
+
}
|
|
874
|
+
catch (e) {
|
|
875
|
+
console.error(`⚠️ wal tick: ${e.message}`);
|
|
876
|
+
}
|
|
877
|
+
this.checkpointWal();
|
|
878
|
+
}, tickMs);
|
|
642
879
|
this.walTimer.unref();
|
|
643
880
|
}
|
|
644
881
|
noteActivity(delayMs = 200) {
|
|
@@ -648,6 +885,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
648
885
|
this.walSoon.unref();
|
|
649
886
|
}
|
|
650
887
|
cleanup() {
|
|
888
|
+
this.shuttingDown = true;
|
|
651
889
|
if (this.walTimer) {
|
|
652
890
|
clearInterval(this.walTimer);
|
|
653
891
|
this.walTimer = null;
|
|
@@ -4127,116 +4365,119 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4127
4365
|
// Validate arguments using our structured schema
|
|
4128
4366
|
const validatedArgs = validateToolArgs(name, args);
|
|
4129
4367
|
ragKgManager.noteActivity(); // fold the WAL shortly after this call, whatever it wrote
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4368
|
+
// An SQLITE_IOERR* reopens the connection; idempotent tools are retried once (wal-sidecar-guard).
|
|
4369
|
+
return await ragKgManager.runWithIoRecovery(name, async () => {
|
|
4370
|
+
switch (name) {
|
|
4371
|
+
// Original MCP tools
|
|
4372
|
+
case "createEntities":
|
|
4373
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.createEntities(validatedArgs.entities), null, 2) }] };
|
|
4374
|
+
case "createRelations":
|
|
4375
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.createRelations(validatedArgs.relations), null, 2) }] };
|
|
4376
|
+
case "addObservations":
|
|
4377
|
+
// v13: status·sources 를 그대로 넘긴다. 여기서 떨어뜨리면 스키마가 받아도
|
|
4378
|
+
// 엔진에 도달하지 않아 provenance 가 조용히 사라진다.
|
|
4379
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.addObservations(validatedArgs.observations), null, 2) }] };
|
|
4380
|
+
// v13 observation lifecycle (spec §6.1 / §6.2)
|
|
4381
|
+
case "correctObservation":
|
|
4382
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: await ragKgManager.correctObservation(validatedArgs.observation_id, validatedArgs.content, validatedArgs.change_kind ?? 'correction', validatedArgs.reason) }, null, 2) }] };
|
|
4383
|
+
case "retractObservation":
|
|
4384
|
+
await ragKgManager.retractObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4385
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'retracted' }, null, 2) }] };
|
|
4386
|
+
case "restoreObservation":
|
|
4387
|
+
await ragKgManager.restoreObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4388
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'active' }, null, 2) }] };
|
|
4389
|
+
case "approveObservation":
|
|
4390
|
+
await ragKgManager.approveObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4391
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'active' }, null, 2) }] };
|
|
4392
|
+
case "declineObservation":
|
|
4393
|
+
await ragKgManager.declineObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4394
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'retracted' }, null, 2) }] };
|
|
4395
|
+
case "purgeObservation":
|
|
4396
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.purgeObservation(validatedArgs.observation_id, validatedArgs.confirm), null, 2) }] };
|
|
4397
|
+
case "getObservationHistory":
|
|
4398
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getObservationHistory({
|
|
4399
|
+
entity_name: validatedArgs.entity_name,
|
|
4400
|
+
observation_id: validatedArgs.observation_id,
|
|
4401
|
+
root_id: validatedArgs.root_id,
|
|
4402
|
+
}), null, 2) }] };
|
|
4403
|
+
case "deleteEntities":
|
|
4404
|
+
await ragKgManager.deleteEntities(validatedArgs.entityNames);
|
|
4405
|
+
return { content: [{ type: "text", text: "Entities deleted successfully" }] };
|
|
4406
|
+
case "deleteObservations":
|
|
4407
|
+
// v3.6 (spec §5c, breaking): structured per-entity results replace the
|
|
4408
|
+
// bare success string — mixed embedded/queued/no-op states are visible.
|
|
4409
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.deleteObservations(validatedArgs.deletions), null, 2) }] };
|
|
4410
|
+
case "deleteRelations":
|
|
4411
|
+
await ragKgManager.deleteRelations(validatedArgs.relations);
|
|
4412
|
+
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
|
4413
|
+
case "updateRelations":
|
|
4414
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.updateRelations(validatedArgs.updates), null, 2) }] };
|
|
4415
|
+
case "readGraph":
|
|
4416
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.readGraph(), null, 2) }] };
|
|
4417
|
+
case "searchNodes":
|
|
4418
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10, validatedArgs.since, validatedArgs.until), null, 2) }] };
|
|
4419
|
+
case "openNodes":
|
|
4420
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
|
|
4421
|
+
case "getNeighbors":
|
|
4422
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getNeighbors(validatedArgs.entityNames, validatedArgs.depth || 1, validatedArgs.relationType), null, 2) }] };
|
|
4423
|
+
// New RAG tools
|
|
4424
|
+
case "storeDocument":
|
|
4425
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.storeDocument(validatedArgs.id, validatedArgs.content, validatedArgs.metadata || {}), null, 2) }] };
|
|
4426
|
+
case "chunkDocument":
|
|
4427
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.chunkDocument(validatedArgs.documentId, { maxTokens: validatedArgs.maxTokens, overlap: validatedArgs.overlap }), null, 2) }] };
|
|
4428
|
+
case "embedChunks":
|
|
4429
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedChunks(validatedArgs.documentId), null, 2) }] };
|
|
4430
|
+
case "extractTerms":
|
|
4431
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.extractTerms(validatedArgs.documentId, { minLength: validatedArgs.minLength, includeCapitalized: validatedArgs.includeCapitalized, customPatterns: validatedArgs.customPatterns }), null, 2) }] };
|
|
4432
|
+
case "linkEntitiesToDocument":
|
|
4433
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.linkEntitiesToDocument(validatedArgs.documentId, validatedArgs.entityNames), null, 2) }] };
|
|
4434
|
+
case "hybridSearch":
|
|
4435
|
+
const limit = typeof validatedArgs.limit === 'number' ? validatedArgs.limit : 5;
|
|
4436
|
+
// v5.3.0: graph is opt-in — only an explicit true enables the re-ranker (schema default false).
|
|
4437
|
+
const useGraph = validatedArgs.useGraph === true;
|
|
4438
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.hybridSearch(validatedArgs.query, limit, useGraph), null, 2) }] };
|
|
4439
|
+
case "getDetailedContext":
|
|
4440
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getDetailedContext(validatedArgs.chunkId, validatedArgs.includeSurrounding !== false), null, 2) }] };
|
|
4441
|
+
case "getKnowledgeGraphStats":
|
|
4442
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getKnowledgeGraphStats(), null, 2) }] };
|
|
4443
|
+
case "deleteDocuments":
|
|
4444
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.deleteDocuments(validatedArgs.documentIds), null, 2) }] };
|
|
4445
|
+
case "listDocuments":
|
|
4446
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.listDocuments(validatedArgs.includeMetadata !== false), null, 2) }] };
|
|
4447
|
+
case "syncDocumentFromFile":
|
|
4448
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.syncDocumentFromFile(validatedArgs.path, validatedArgs.documentId, {
|
|
4449
|
+
metadata: validatedArgs.metadata,
|
|
4450
|
+
content: validatedArgs.content,
|
|
4451
|
+
excludePattern: validatedArgs.excludePattern,
|
|
4452
|
+
entityNames: validatedArgs.entityNames,
|
|
4453
|
+
chunkParams: validatedArgs.chunkParams,
|
|
4454
|
+
}), null, 2) }] };
|
|
4455
|
+
// NEW: Entity embedding tools
|
|
4456
|
+
case "embedAllEntities":
|
|
4457
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedAllEntities(), null, 2) }] };
|
|
4458
|
+
// NEW: Export/Import tools
|
|
4459
|
+
case "exportGraph":
|
|
4460
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.exportGraph(), null, 2) }] };
|
|
4461
|
+
case "importGraph":
|
|
4462
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.importGraph(validatedArgs.data, { merge: validatedArgs.merge !== false }), null, 2) }] };
|
|
4463
|
+
// NEW: Migration tools
|
|
4464
|
+
case "getMigrationStatus":
|
|
4465
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getMigrationStatus(), null, 2) }] };
|
|
4466
|
+
case "runMigrations":
|
|
4467
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.runMigrations(), null, 2) }] };
|
|
4468
|
+
case "rollbackMigration":
|
|
4469
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.rollbackMigration(validatedArgs.targetVersion), null, 2) }] };
|
|
4470
|
+
// Graph Analytics tools (graphology)
|
|
4471
|
+
case "getGraphMetrics":
|
|
4472
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getGraphMetrics(validatedArgs.entityNames, validatedArgs.metrics, validatedArgs.limit || 10), null, 2) }] };
|
|
4473
|
+
case "detectCommunities":
|
|
4474
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.detectCommunities(validatedArgs.resolution || 1.0), null, 2) }] };
|
|
4475
|
+
case "analyzeGraphStructure":
|
|
4476
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.analyzeGraphStructure(), null, 2) }] };
|
|
4477
|
+
default:
|
|
4478
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
4479
|
+
}
|
|
4480
|
+
});
|
|
4240
4481
|
}
|
|
4241
4482
|
catch (error) {
|
|
4242
4483
|
// v3.6 (spec §5c): machine-distinguishable failures. Embedding-gate errors
|
|
@@ -4301,6 +4542,7 @@ async function main() {
|
|
|
4301
4542
|
if (shuttingDown)
|
|
4302
4543
|
return;
|
|
4303
4544
|
shuttingDown = true;
|
|
4545
|
+
ragKgManager.shuttingDown = true; // no reopen inside the exit window (wal-sidecar-guard)
|
|
4304
4546
|
// Synchronously, before anything is awaited: a host may SIGKILL before the settle
|
|
4305
4547
|
// sequence below completes (codex: ~185 ms after SIGTERM).
|
|
4306
4548
|
ragKgManager.checkpointWal();
|
|
@@ -4325,6 +4567,7 @@ async function main() {
|
|
|
4325
4567
|
// Ctrl+Break; registering them is harmless where they never fire.
|
|
4326
4568
|
process.on('SIGHUP', shutdown);
|
|
4327
4569
|
process.on('SIGBREAK', shutdown);
|
|
4570
|
+
ragKgManager.onFatal = shutdown; // a database that cannot be reopened ends the server (wal-sidecar-guard)
|
|
4328
4571
|
ragKgManager.startWalKeeper();
|
|
4329
4572
|
process.on('exit', () => { try {
|
|
4330
4573
|
ragKgManager.cleanup();
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
// kick() merely triggers a debounced scan. All reconciliation writes are
|
|
15
15
|
// per-row transactions with a CAS re-check so a foreground embed that lands
|
|
16
16
|
// first is never downgraded (6R note 3).
|
|
17
|
+
// A handle closed by the engine's reopen (wal-sidecar-guard), or an I/O error from a sidecar the
|
|
18
|
+
// sync client swapped out: the run was cut, not broken.
|
|
19
|
+
const CONN_LOSS_RERUN_MAX = 3;
|
|
20
|
+
function isConnectionLoss(e) {
|
|
21
|
+
const code = e?.code;
|
|
22
|
+
if (typeof code === 'string' && code.startsWith('SQLITE_IOERR'))
|
|
23
|
+
return true;
|
|
24
|
+
return e instanceof Error && /database connection is not open/i.test(e.message);
|
|
25
|
+
}
|
|
17
26
|
const RECON_BATCH = 50;
|
|
18
27
|
const KICK_DEBOUNCE_MS = 200;
|
|
19
28
|
const SWEEP_MS = 5 * 60_000;
|
|
@@ -25,6 +34,7 @@ export class BackfillCoordinator {
|
|
|
25
34
|
kickTimer = null;
|
|
26
35
|
sweepTimer = null;
|
|
27
36
|
shuttingDown = false;
|
|
37
|
+
connLossReruns = 0;
|
|
28
38
|
scanning = false;
|
|
29
39
|
scanPromise = null;
|
|
30
40
|
rerunRequested = false;
|
|
@@ -178,6 +188,7 @@ export class BackfillCoordinator {
|
|
|
178
188
|
if (remaining !== 0)
|
|
179
189
|
throw new Error(`complete invariant violated: ${remaining} unreconciled vectors remain`);
|
|
180
190
|
this.recon = 'complete';
|
|
191
|
+
this.connLossReruns = 0;
|
|
181
192
|
this.snapshot = null;
|
|
182
193
|
console.error('✅ provenance reconciliation complete');
|
|
183
194
|
this.kick();
|
|
@@ -190,6 +201,18 @@ export class BackfillCoordinator {
|
|
|
190
201
|
this.reconPromise = null;
|
|
191
202
|
return;
|
|
192
203
|
}
|
|
204
|
+
if (isConnectionLoss(e) && this.connLossReruns < CONN_LOSS_RERUN_MAX) {
|
|
205
|
+
// The engine reopened the connection under us (wal-sidecar-guard). Not broken: run
|
|
206
|
+
// again on the new handle. If there is no handle right now (reopen failed), the engine
|
|
207
|
+
// starts reconciliation itself once it reopens.
|
|
208
|
+
this.connLossReruns++;
|
|
209
|
+
this.recon = 'pending';
|
|
210
|
+
this.reconPromise = null;
|
|
211
|
+
if (this.deps.db())
|
|
212
|
+
setTimeout(() => { void this.runReconciliation(); }, 0).unref?.();
|
|
213
|
+
console.error(`↻ reconciliation interrupted by a reconnect — rerun ${this.connLossReruns}/${CONN_LOSS_RERUN_MAX}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
193
216
|
this.recon = 'failed';
|
|
194
217
|
this.reconError = e instanceof Error ? e.message : String(e);
|
|
195
218
|
console.error(`❌ reconciliation failed — vector search stays disabled: ${this.reconError}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24"
|
|
6
6
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"prepare": "npm run build",
|
|
46
46
|
"watch": "tsc --watch",
|
|
47
47
|
"verify:invariants": "node test/chunk-invariants.test.mjs",
|
|
48
|
-
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/alias-link-gate.test.mjs && node test/entity-name-boundary.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs && node test/delete-entities-kg-hygiene.test.mjs && node test/backup-publish-portable.test.mjs && node test/graph-context-explain.test.mjs && node test/eval-graph-role-libs.test.mjs && node test/eval-graph-role-t5b.test.mjs && node --test test/eval-graph-role-t8-fix.test.mjs && node --test test/eval-graph-role-t7-upstream.test.mjs && node --test test/eval-graph-role-t11-decision.test.mjs && node --test test/eval-graph-role-prereq-fix.test.mjs && node test/db-path.test.mjs && node test/mmap-option.test.mjs && node test/wal-exit.test.mjs",
|
|
48
|
+
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/alias-link-gate.test.mjs && node test/entity-name-boundary.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs && node test/delete-entities-kg-hygiene.test.mjs && node test/backup-publish-portable.test.mjs && node test/graph-context-explain.test.mjs && node test/eval-graph-role-libs.test.mjs && node test/eval-graph-role-t5b.test.mjs && node --test test/eval-graph-role-t8-fix.test.mjs && node --test test/eval-graph-role-t7-upstream.test.mjs && node --test test/eval-graph-role-t11-decision.test.mjs && node --test test/eval-graph-role-prereq-fix.test.mjs && node test/db-path.test.mjs && node test/mmap-option.test.mjs && node test/wal-exit.test.mjs && node test/wal-sidecar-guard.test.mjs",
|
|
49
49
|
"test": "npm run build && npm run verify:invariants && npm run verify:engine",
|
|
50
50
|
"prepublishOnly": "npm run build && npm run verify:invariants && npm run verify:engine"
|
|
51
51
|
},
|