rag-memory-epf-mcp 6.3.0 → 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 +40 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +461 -142
- 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,46 @@ 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
|
+
|
|
181
|
+
### v6.3.1
|
|
182
|
+
|
|
183
|
+
- **Changed — the engine empties the WAL while it is idle.** It runs `wal_checkpoint(TRUNCATE)` itself:
|
|
184
|
+
on a 1 s tick whenever the `-wal` file has bytes in it, ~200 ms after a tool call, first thing in the
|
|
185
|
+
signal handler, and before closing. SQLite folds the WAL only when the *last* connection closes and
|
|
186
|
+
only if that close gets to run; with several engines on one file, or a host that kills the engine
|
|
187
|
+
(measured: codex-cli 0.155.1 sends SIGTERM and SIGKILLs ~185 ms later; Windows terminates children
|
|
188
|
+
outright), frames stayed in the WAL — on 6.3.0 a fresh database sat at 4 KB main + 663 KB WAL for as
|
|
189
|
+
long as the engine ran. In a cloud-synced folder such a WAL can be replayed onto a main file written
|
|
190
|
+
by another machine. A write made in the last ~1 s before a hard kill can still be in the WAL; a 0-byte
|
|
191
|
+
`-wal` and a `-shm` left behind by a hard kill are harmless. Running engines on two machines against
|
|
192
|
+
the same synced file at the same time is still unsafe.
|
|
193
|
+
- **Changed — SIGHUP and SIGBREAK shut the engine down cleanly.** Closing the terminal or multiplexer
|
|
194
|
+
pane used to kill the process by default signal action, before the database was closed.
|
|
195
|
+
- No tool, argument, return shape or schema changed.
|
|
196
|
+
|
|
157
197
|
### v6.3.0 (2026-09-16)
|
|
158
198
|
|
|
159
199
|
- **Changed — a relative `DB_FILE_PATH` now resolves against the server's working directory.** It used
|
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>;
|
|
@@ -101,6 +103,31 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
101
103
|
description: string;
|
|
102
104
|
}>;
|
|
103
105
|
}>;
|
|
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>;
|
|
127
|
+
private walTimer;
|
|
128
|
+
private walSoon;
|
|
129
|
+
startWalKeeper(tickMs?: number): void;
|
|
130
|
+
noteActivity(delayMs?: number): void;
|
|
104
131
|
cleanup(): void;
|
|
105
132
|
private _timestampObservation;
|
|
106
133
|
createEntities(entities: Array<Entity & {
|
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);
|
|
@@ -593,13 +626,283 @@ export class RAGKnowledgeGraphManager {
|
|
|
593
626
|
}))
|
|
594
627
|
};
|
|
595
628
|
}
|
|
629
|
+
// Fold the WAL into the main file and empty it. Returns true when the WAL holds no
|
|
630
|
+
// frames afterwards.
|
|
631
|
+
//
|
|
632
|
+
// Why the engine does this itself instead of leaving it to SQLite: SQLite folds the WAL
|
|
633
|
+
// only when the LAST connection closes, and only if that close gets to run. Neither holds
|
|
634
|
+
// here — several engines keep the same file open (one per CLI), and some hosts end the
|
|
635
|
+
// engine without letting it finish (codex-cli 0.155.1, measured: SIGTERM, then SIGKILL
|
|
636
|
+
// ~185 ms later; on Windows a child is simply terminated). A WAL with frames that outlives
|
|
637
|
+
// its process is replayed at the next open against whatever main file is there by then, and
|
|
638
|
+
// in a cloud-synced folder that can be a main file written by another machine. SQLite does
|
|
639
|
+
// not check that a WAL belongs to the main file next to it. That corrupted a live database
|
|
640
|
+
// twice. So the invariant is kept continuously: while idle, the WAL is empty.
|
|
641
|
+
//
|
|
642
|
+
// TRUNCATE, not PASSIVE: PASSIVE copies the frames into the main file but leaves them in
|
|
643
|
+
// the WAL, still valid, still replayable onto a foreign main file.
|
|
644
|
+
// busy_timeout is dropped to 0 around the call: TRUNCATE runs the busy handler, and a 5 s
|
|
645
|
+
// stall inside a signal handler or a timer tick is worse than retrying on the next tick.
|
|
646
|
+
checkpointWal() {
|
|
647
|
+
if (!this.db)
|
|
648
|
+
return true;
|
|
649
|
+
try {
|
|
650
|
+
const st = fsSync.statSync(DB_FILE_PATH + '-wal', { throwIfNoEntry: false });
|
|
651
|
+
if (!st || st.size === 0)
|
|
652
|
+
return true;
|
|
653
|
+
this.db.pragma('busy_timeout = 0');
|
|
654
|
+
try {
|
|
655
|
+
const r = this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
656
|
+
return r?.[0]?.busy === 0;
|
|
657
|
+
}
|
|
658
|
+
finally {
|
|
659
|
+
this.db.pragma('busy_timeout = 5000');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
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);
|
|
667
|
+
return false; // inside a transaction, or the file is locked — the next tick retries
|
|
668
|
+
}
|
|
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
|
+
}
|
|
862
|
+
walTimer = null;
|
|
863
|
+
walSoon = null;
|
|
864
|
+
// Every write path ends up here without having to be listed: the periodic tick looks at
|
|
865
|
+
// the size of the -wal file, not at which tool ran, so background writers (reconciliation,
|
|
866
|
+
// backfill) are covered as well. noteActivity() only shortens the wait after a tool call.
|
|
867
|
+
startWalKeeper(tickMs = 1000) {
|
|
868
|
+
if (this.walTimer)
|
|
869
|
+
return;
|
|
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);
|
|
879
|
+
this.walTimer.unref();
|
|
880
|
+
}
|
|
881
|
+
noteActivity(delayMs = 200) {
|
|
882
|
+
if (this.walSoon)
|
|
883
|
+
return;
|
|
884
|
+
this.walSoon = setTimeout(() => { this.walSoon = null; this.checkpointWal(); }, delayMs);
|
|
885
|
+
this.walSoon.unref();
|
|
886
|
+
}
|
|
596
887
|
cleanup() {
|
|
888
|
+
this.shuttingDown = true;
|
|
889
|
+
if (this.walTimer) {
|
|
890
|
+
clearInterval(this.walTimer);
|
|
891
|
+
this.walTimer = null;
|
|
892
|
+
}
|
|
893
|
+
if (this.walSoon) {
|
|
894
|
+
clearTimeout(this.walSoon);
|
|
895
|
+
this.walSoon = null;
|
|
896
|
+
}
|
|
597
897
|
if (this.encoding) {
|
|
598
898
|
this.encoding.free();
|
|
599
899
|
this.encoding = null;
|
|
600
900
|
}
|
|
601
901
|
this.embeddingCache.clear();
|
|
602
902
|
if (this.db) {
|
|
903
|
+
// close() folds the WAL only for the last connection; with other engines attached it
|
|
904
|
+
// would leave every frame behind.
|
|
905
|
+
this.checkpointWal();
|
|
603
906
|
this.db.close();
|
|
604
907
|
this.db = null;
|
|
605
908
|
}
|
|
@@ -4061,116 +4364,120 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4061
4364
|
try {
|
|
4062
4365
|
// Validate arguments using our structured schema
|
|
4063
4366
|
const validatedArgs = validateToolArgs(name, args);
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
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
|
-
|
|
4367
|
+
ragKgManager.noteActivity(); // fold the WAL shortly after this call, whatever it wrote
|
|
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
|
+
});
|
|
4174
4481
|
}
|
|
4175
4482
|
catch (error) {
|
|
4176
4483
|
// v3.6 (spec §5c): machine-distinguishable failures. Embedding-gate errors
|
|
@@ -4235,6 +4542,10 @@ async function main() {
|
|
|
4235
4542
|
if (shuttingDown)
|
|
4236
4543
|
return;
|
|
4237
4544
|
shuttingDown = true;
|
|
4545
|
+
ragKgManager.shuttingDown = true; // no reopen inside the exit window (wal-sidecar-guard)
|
|
4546
|
+
// Synchronously, before anything is awaited: a host may SIGKILL before the settle
|
|
4547
|
+
// sequence below completes (codex: ~185 ms after SIGTERM).
|
|
4548
|
+
ragKgManager.checkpointWal();
|
|
4238
4549
|
void (async () => {
|
|
4239
4550
|
try {
|
|
4240
4551
|
await server.close();
|
|
@@ -4250,6 +4561,14 @@ async function main() {
|
|
|
4250
4561
|
};
|
|
4251
4562
|
process.on('SIGINT', shutdown);
|
|
4252
4563
|
process.on('SIGTERM', shutdown);
|
|
4564
|
+
// SIGHUP = the terminal or multiplexer pane was closed. Without a handler the default
|
|
4565
|
+
// action kills the process before 'exit' listeners run, so the database is never closed.
|
|
4566
|
+
// On Windows, Node raises SIGHUP when the console window is closed and SIGBREAK on
|
|
4567
|
+
// Ctrl+Break; registering them is harmless where they never fire.
|
|
4568
|
+
process.on('SIGHUP', shutdown);
|
|
4569
|
+
process.on('SIGBREAK', shutdown);
|
|
4570
|
+
ragKgManager.onFatal = shutdown; // a database that cannot be reopened ends the server (wal-sidecar-guard)
|
|
4571
|
+
ragKgManager.startWalKeeper();
|
|
4253
4572
|
process.on('exit', () => { try {
|
|
4254
4573
|
ragKgManager.cleanup();
|
|
4255
4574
|
}
|
|
@@ -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",
|
|
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
|
},
|