rag-memory-epf-mcp 6.3.1 → 6.4.0-rc.1
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 +44 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +436 -158
- package/dist/src/backfillCoordinator.d.ts +1 -0
- package/dist/src/backfillCoordinator.js +23 -0
- package/dist/src/tools/knowledge-graph-tools.js +9 -7
- package/docs/UPDATING.md +50 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,6 +154,50 @@ storeDocument(id, content, metadata)
|
|
|
154
154
|
|
|
155
155
|
## Changelog
|
|
156
156
|
|
|
157
|
+
### v6.4.0 (2026-10-03)
|
|
158
|
+
|
|
159
|
+
- (Release candidate `6.4.0-rc.1` on the `next` dist-tag; `latest` stays 6.3.2 until the published artifact passes a
|
|
160
|
+
canary against a real project database.)
|
|
161
|
+
|
|
162
|
+
- **Changed** `hybridSearch` default ranking = reciprocal rank fusion of the vector and FTS5 candidate lists
|
|
163
|
+
(k=60). A chunk that only FTS5 finds — typically an exact identifier, filename or setting key — can now reach
|
|
164
|
+
the top; before, it scored at most 0.0164 and sat under every vector candidate in the measured corpora.
|
|
165
|
+
`relevance_score` on the default call is a rank score (at most 2/61, comparable only within one call). Measured on frozen snapshots: exact-identifier hit@10 7/20 → 18/20; worse on 0 of
|
|
166
|
+
203 queries across three corpora.
|
|
167
|
+
- **Changed** the per-result summary path is opt-in (`RAG_MEMORY_SEARCH_SUMMARIES=on`). It embedded every
|
|
168
|
+
sentence fragment of every candidate (median 121 s per search) and its context boost pushed vector-0 chunks
|
|
169
|
+
to rank 1. `off` keeps working. Legacy paths (`summaries=on`, `useGraph: true`) keep the 6.3.2 formula.
|
|
170
|
+
- **Changed** with the default model (bge-m3) queries are embedded as raw text: the bge-v1.5 instruction the loader
|
|
171
|
+
prepended to every query is gone (bge-m3 needs none; stored vectors never had it). A custom `EMBEDDING_MODEL` keeps
|
|
172
|
+
the previous query instruction. Paraphrase hit@10 7 → 9 of 20, identifier hit@1 7 → 8,
|
|
173
|
+
document known-item (`hybridSearch`) unchanged or +1; entity known-item (`searchNodes`) hit@10 unchanged,
|
|
174
|
+
hit@1 -1 / -1 / +1 (hub / uap / hal). No re-index.
|
|
175
|
+
- Details and rollout: `docs/UPDATING.md`, `specs/changes/search-fusion-rrf/proposal.md`.
|
|
176
|
+
|
|
177
|
+
### v6.3.2 (2026-09-25)
|
|
178
|
+
|
|
179
|
+
- **Changed — on macOS the engine takes `-wal` and `-shm` out of cloud sync.** It sets
|
|
180
|
+
`com.apple.fileprovider.ignore#P = 1` on both files after every open (SQLite recreates them after the
|
|
181
|
+
last connection closes, so once is not enough). Measured with Google Drive for desktop: the client
|
|
182
|
+
treated `-wal` as a synced item and put a new file at the path while the engine held the old one open
|
|
183
|
+
(`lsof -p` showed the descriptor inside `FileProvider/…/wharf/delete/`); from then on every write
|
|
184
|
+
failed with `SQLITE_IOERR` ("disk I/O error") until the engine restarted — 10 s to 3 min after start,
|
|
185
|
+
with one engine attached. With the attribute set, the same live database kept the same `-wal` for
|
|
186
|
+
25 min of use. The main database file still syncs. Other platforms: no-op.
|
|
187
|
+
- **Changed — a swapped sidecar is detected and the connection reopened.** The idle tick compares the
|
|
188
|
+
`-wal`/`-shm` inodes with the ones recorded at open (two consecutive mismatches) and reopens with the
|
|
189
|
+
same configuration; an `SQLITE_IOERR*` from a checkpoint or a tool call reopens too. A tool that hit
|
|
190
|
+
the I/O error is retried once only if repeating it changes nothing (reads, `createEntities`,
|
|
191
|
+
`createRelations`, `addObservations`, `deleteEntities`, `deleteRelations`); other tools get the
|
|
192
|
+
error back after the reopen. A reopen waits for running tool calls, is not done while shutting
|
|
193
|
+
down, and is braked after 3 in 10 s. A failed reopen retries with backoff and, after 10 failures,
|
|
194
|
+
shuts the server down so the host shows it as failed. A reconciliation cut by a reopen reruns.
|
|
195
|
+
- Known limit: writes acknowledged in the last ~1 s before a swap can be lost if the old file is no
|
|
196
|
+
longer readable (the engine tries one checkpoint on the old handle first and logs `walsz-before=`).
|
|
197
|
+
- The 6.3.1 `wal_checkpoint(TRUNCATE)` is unchanged. `RESTART` (± `journal_size_limit`) was measured and
|
|
198
|
+
rejected: after SIGKILL its WAL replayed onto a foreign main file 3/3 times.
|
|
199
|
+
- No tool, argument, return shape or schema changed. Spec: `specs/changes/wal-sidecar-guard/`.
|
|
200
|
+
|
|
157
201
|
### v6.3.1
|
|
158
202
|
|
|
159
203
|
- **Changed — the engine empties the WAL while it is idle.** It runs `wal_checkpoint(TRUNCATE)` itself:
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { type SourceInput } from './src/observations/lifecycle.js';
|
|
|
3
3
|
import { EmbeddingGate } from './src/embeddingGate.js';
|
|
4
4
|
import type { EmbedPriority } from './src/embeddingGate.js';
|
|
5
5
|
import { BackfillCoordinator } from './src/backfillCoordinator.js';
|
|
6
|
+
export declare const LEGACY_QUERY_INSTRUCTION = "Represent this sentence for searching relevant passages: ";
|
|
7
|
+
export declare function embeddingInput(text: string, isQuery: boolean, defaultModel?: boolean): string;
|
|
8
|
+
export declare function makeEmbedFn(model: (input: string, opts: any) => Promise<{
|
|
9
|
+
data: any;
|
|
10
|
+
}>, defaultModel: boolean): (text: string, dims: number, isQuery: boolean) => Promise<Float32Array<ArrayBuffer>>;
|
|
6
11
|
export declare function compileFtsLiteralQuery(q: string): string | null;
|
|
7
12
|
export declare function resolveDbFilePath(envValue: string | undefined, cwd: string, serverDir: string): string;
|
|
8
13
|
export declare function parseMmapSize(raw: string | undefined): number;
|
|
@@ -51,6 +56,7 @@ interface DetailedContext {
|
|
|
51
56
|
export declare class SyncCasConflictError extends Error {
|
|
52
57
|
constructor(documentId: string);
|
|
53
58
|
}
|
|
59
|
+
export declare function isIoError(e: unknown): boolean;
|
|
54
60
|
export declare function setSyncFaultPoint(point: string | null, fn: (() => void) | null): void;
|
|
55
61
|
export interface ReuseCandidate {
|
|
56
62
|
rowid: number;
|
|
@@ -82,6 +88,7 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
82
88
|
gate?: EmbeddingGate;
|
|
83
89
|
__testForceFkOff?: boolean;
|
|
84
90
|
}): Promise<void>;
|
|
91
|
+
private openConnection;
|
|
85
92
|
private ensureCurrentProfile;
|
|
86
93
|
private buildRealLoader;
|
|
87
94
|
startReconciliation(): Promise<void>;
|
|
@@ -102,6 +109,26 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
102
109
|
}>;
|
|
103
110
|
}>;
|
|
104
111
|
checkpointWal(): boolean;
|
|
112
|
+
walIno: bigint | null;
|
|
113
|
+
shmIno: bigint | null;
|
|
114
|
+
reopenCount: number;
|
|
115
|
+
private reopenTimes;
|
|
116
|
+
private inFlight;
|
|
117
|
+
private pendingReopen;
|
|
118
|
+
private walMismatchTicks;
|
|
119
|
+
private walWatchPausedUntil;
|
|
120
|
+
private walWatchOff;
|
|
121
|
+
private lastWalSize;
|
|
122
|
+
private dbDown;
|
|
123
|
+
shuttingDown: boolean;
|
|
124
|
+
onFatal: (() => void) | null;
|
|
125
|
+
guardSidecars(): void;
|
|
126
|
+
walTick(): boolean;
|
|
127
|
+
private stormCount;
|
|
128
|
+
reopenDb(reason: string, force?: boolean): boolean;
|
|
129
|
+
private openAfterClose;
|
|
130
|
+
private retryOpen;
|
|
131
|
+
runWithIoRecovery<T>(tool: string, fn: () => Promise<T>): Promise<T>;
|
|
105
132
|
private walTimer;
|
|
106
133
|
private walSoon;
|
|
107
134
|
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';
|
|
@@ -66,6 +67,24 @@ function printBanner(opts) {
|
|
|
66
67
|
// operators (every term is double-quoted; special characters stripped exactly
|
|
67
68
|
// as the pre-3.6 hybridSearch sanitizer did). Returns null when nothing
|
|
68
69
|
// searchable remains (contract: caller returns empty results + warning).
|
|
70
|
+
// search-fusion-rrf (query input): what the model sees for a text. For the DEFAULT model (bge-m3) queries are
|
|
71
|
+
// embedded as raw text — the model card: "the BGE-M3 model no longer requires adding instructions to the
|
|
72
|
+
// queries"; measured on the frozen hub snapshot (93 queries, full-corpus vector rank): 36 better, 6 worse, 51 same.
|
|
73
|
+
// A CUSTOM model (EMBEDDING_MODEL set to something else) keeps the pre-change query instruction: the evidence
|
|
74
|
+
// above is for bge-m3 only, and some models (e.g. bge v1.5) still recommend an instruction on queries.
|
|
75
|
+
// Passages are always raw. `isQuery` stays in the signature and in the embedding cache key.
|
|
76
|
+
export const LEGACY_QUERY_INSTRUCTION = 'Represent this sentence for searching relevant passages: ';
|
|
77
|
+
export function embeddingInput(text, isQuery, defaultModel = true) {
|
|
78
|
+
return isQuery && !defaultModel ? `${LEGACY_QUERY_INSTRUCTION}${text}` : text;
|
|
79
|
+
}
|
|
80
|
+
// The embed function handed to the gate. Exported so the input wiring is testable with a stub model.
|
|
81
|
+
export function makeEmbedFn(model, defaultModel) {
|
|
82
|
+
return async (text, dims, isQuery) => {
|
|
83
|
+
const input = embeddingInput(text, isQuery, defaultModel);
|
|
84
|
+
const r = await model(input, { pooling: 'cls', normalize: true });
|
|
85
|
+
return new Float32Array(r.data.slice(0, dims));
|
|
86
|
+
};
|
|
87
|
+
}
|
|
69
88
|
export function compileFtsLiteralQuery(q) {
|
|
70
89
|
const sanitized = q.replace(/["\*\(\)\-]/g, ' ').trim();
|
|
71
90
|
if (!sanitized)
|
|
@@ -136,6 +155,30 @@ const TEXT_BUILDER_VERSION = 'tb1';
|
|
|
136
155
|
export class SyncCasConflictError extends Error {
|
|
137
156
|
constructor(documentId) { super(`sync CAS conflict on ${documentId}`); this.name = 'SyncCasConflictError'; }
|
|
138
157
|
}
|
|
158
|
+
// Sidecar guard (specs/changes/wal-sidecar-guard). macOS File Provider attribute that takes an
|
|
159
|
+
// item out of sync; Google Drive for desktop honours it (measured: isExcludedFromSync = 1).
|
|
160
|
+
const SYNC_EXCLUDE_ATTR = 'com.apple.fileprovider.ignore#P';
|
|
161
|
+
const REOPEN_STORM_WINDOW_MS = 10_000;
|
|
162
|
+
const REOPEN_STORM_MAX = 3;
|
|
163
|
+
const REOPEN_MAX_TRIES = 10;
|
|
164
|
+
const WAL_WATCH_PAUSE_MS = 60_000;
|
|
165
|
+
// Tools that may be retried once after an I/O error reopened the connection: reads, and writes
|
|
166
|
+
// whose repetition changes nothing (INSERT OR IGNORE / content dedup / deletes). Everything else
|
|
167
|
+
// gets the error back so the caller decides — e.g. syncDocumentFromFile spans several
|
|
168
|
+
// transactions and its repeat-safety is not proven.
|
|
169
|
+
const IDEMPOTENT_TOOLS = new Set([
|
|
170
|
+
'readGraph', 'searchNodes', 'openNodes', 'getNeighbors', 'hybridSearch', 'getDetailedContext',
|
|
171
|
+
'getKnowledgeGraphStats', 'listDocuments', 'exportGraph', 'getMigrationStatus', 'getGraphMetrics',
|
|
172
|
+
'detectCommunities', 'analyzeGraphStructure', 'getObservationHistory',
|
|
173
|
+
'createEntities', 'createRelations', 'addObservations',
|
|
174
|
+
// deleteObservations / deleteDocuments are left out on purpose: a retry after a committed
|
|
175
|
+
// delete reports 0 deleted, which a caller reads as "nothing was deleted".
|
|
176
|
+
'deleteEntities', 'deleteRelations',
|
|
177
|
+
]);
|
|
178
|
+
export function isIoError(e) {
|
|
179
|
+
const code = e?.code;
|
|
180
|
+
return typeof code === 'string' && code.startsWith('SQLITE_IOERR');
|
|
181
|
+
}
|
|
139
182
|
// Test-only fault hook (v13 setMigrationFaultPoint 선례 — 환경변수 금지: 상시 스위치는
|
|
140
183
|
// 오설정 한 줄로 sync 를 깬다).
|
|
141
184
|
let __syncFaultHook = null;
|
|
@@ -219,38 +262,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
219
262
|
// 호출부에서 읽히게 하기 위해서다.
|
|
220
263
|
async initialize(opts = {}) {
|
|
221
264
|
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
|
-
}
|
|
265
|
+
this.openConnection(opts.__testForceFkOff === true);
|
|
254
266
|
this.encoding = get_encoding("cl100k_base");
|
|
255
267
|
await this.runMigrations();
|
|
256
268
|
this.currentProfileId = this.ensureCurrentProfile();
|
|
@@ -259,6 +271,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
259
271
|
this.db.prepare(`INSERT INTO server_meta (key, value) VALUES ('current_default_chunker', ?)
|
|
260
272
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
261
273
|
.run(effectiveSignature(DEFAULT_MAX_TOKENS));
|
|
274
|
+
// The writes above created the sidecars; mark them before a sync client can take them.
|
|
275
|
+
this.guardSidecars();
|
|
262
276
|
this.embeddingsMode = opts.skipModel
|
|
263
277
|
? 'off'
|
|
264
278
|
: (process.env.RAG_MEMORY_EMBEDDINGS || 'lazy');
|
|
@@ -287,6 +301,42 @@ export class RAGKnowledgeGraphManager {
|
|
|
287
301
|
const systemInfo = getSystemInfo();
|
|
288
302
|
console.error(`📊 System Info: ${systemInfo.toolCounts.total} tools available (${systemInfo.toolCounts.knowledgeGraph} knowledge graph, ${systemInfo.toolCounts.rag} RAG, ${systemInfo.toolCounts.graphQuery} query)`);
|
|
289
303
|
}
|
|
304
|
+
// Opens the connection and applies everything the engine attaches to it. Shared by
|
|
305
|
+
// initialize() and reopenDb() so a reopened connection is configured exactly like the first.
|
|
306
|
+
openConnection(forceFkOff = false) {
|
|
307
|
+
this.db = new Database(DB_FILE_PATH);
|
|
308
|
+
sqliteVec.load(this.db);
|
|
309
|
+
this.db.pragma('journal_mode = WAL');
|
|
310
|
+
this.db.pragma('synchronous = NORMAL');
|
|
311
|
+
this.db.pragma('busy_timeout = 5000');
|
|
312
|
+
this.db.pragma('cache_size = -32000');
|
|
313
|
+
this.db.pragma('temp_store = MEMORY');
|
|
314
|
+
// Opt-in mmap (see MMAP_SIZE above). Read the applied value back so the banner reports
|
|
315
|
+
// what SQLite did, not what we asked for (a compile-time cap can lower it silently).
|
|
316
|
+
this.db.pragma(`mmap_size = ${MMAP_SIZE}`);
|
|
317
|
+
this.mmapApplied = Number(this.db.pragma('mmap_size', { simple: true }));
|
|
318
|
+
this.db.pragma('foreign_keys = ON');
|
|
319
|
+
// spec §5.2: 관찰 lifecycle 의 무결성은 전부 FK CASCADE 를 전제한다 — root 를 지우면
|
|
320
|
+
// revision 이, revision 을 지우면 source 가 따라가야 history 가 고아로 남지 않는다.
|
|
321
|
+
// FK 가 꺼진 채 돌면 그 계약이 조용히 무효가 되고, 그게 최악이다. 스키마를 건드리기
|
|
322
|
+
// 전에(= runMigrations 앞에서) 멈춘다.
|
|
323
|
+
// 트랜잭션 내부에서는 이 pragma 가 no-op 이므로(실측 before=1·during=1·after=1)
|
|
324
|
+
// 부팅 시점 확인이 유일한 방어 지점이다.
|
|
325
|
+
//
|
|
326
|
+
// 음성 대조군 주입은 **인자로만** 받는다. 환경변수로 두면 프로덕션 경로에
|
|
327
|
+
// "부팅을 막는 스위치"가 상시 존재하게 되고, 오설정 한 줄로 서버가 안 뜬다
|
|
328
|
+
// (advisor beta 자기의심 2 = "더 나쁘다"). 테스트는 manager 를 직접 만들므로
|
|
329
|
+
// 인자 주입으로 충분하다.
|
|
330
|
+
if (forceFkOff)
|
|
331
|
+
this.db.pragma('foreign_keys = OFF');
|
|
332
|
+
{
|
|
333
|
+
const fk = this.db.pragma('foreign_keys', { simple: true });
|
|
334
|
+
if (Number(fk) !== 1) {
|
|
335
|
+
throw new Error(`foreign_keys is ${fk}, expected 1. The observation lifecycle relies on FK CASCADE ` +
|
|
336
|
+
`for history integrity; refusing to run migrations without it.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
290
340
|
// Upsert the stored-vector compatibility profile (spec §6c layer 2) and record
|
|
291
341
|
// retrieval config in server_meta (layer 3 — never a backfill trigger).
|
|
292
342
|
ensureCurrentProfile() {
|
|
@@ -303,8 +353,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
303
353
|
.get(EMBEDDING_MODEL, MODEL_REVISION, MODEL_DTYPE, dims, 'cls', 1);
|
|
304
354
|
this.db.prepare(`INSERT INTO server_meta(key,value) VALUES('current_profile_id',?)
|
|
305
355
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(String(row.id));
|
|
306
|
-
|
|
307
|
-
|
|
356
|
+
// query_prefix_version: diagnostic marker of the query-input convention of the engine that last INITIALISED
|
|
357
|
+
// this DB — 1 = instruction prepended to queries (<= 6.3.2, and custom models now), 2 = raw queries (default
|
|
358
|
+
// bge-m3 since search-fusion-rrf). It is written at init, not per query: it does not prove what the last query
|
|
359
|
+
// used, which engines are attached, or vector compatibility. Nothing reads it for logic.
|
|
360
|
+
this.db.prepare(`INSERT INTO server_meta(key,value) VALUES('query_prefix_version',?)
|
|
361
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(IS_DEFAULT_MODEL_CONFIG ? '2' : '1');
|
|
308
362
|
return row.id;
|
|
309
363
|
}
|
|
310
364
|
// Real model loader used by the gate: version-independent cache dir with a
|
|
@@ -342,11 +396,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
342
396
|
if (role === 'owner')
|
|
343
397
|
lock.markComplete();
|
|
344
398
|
console.error(`✅ ${EMBEDDING_MODEL} model loaded (${MODEL_DTYPE})`);
|
|
345
|
-
return
|
|
346
|
-
const input = isQuery ? `Represent this sentence for searching relevant passages: ${text}` : text;
|
|
347
|
-
const r = await model(input, { pooling: 'cls', normalize: true });
|
|
348
|
-
return new Float32Array(r.data.slice(0, dims));
|
|
349
|
-
};
|
|
399
|
+
return makeEmbedFn(model, IS_DEFAULT_MODEL_CONFIG);
|
|
350
400
|
}
|
|
351
401
|
catch (e) {
|
|
352
402
|
// Cache policy by CAUSE and ROLE (beta 2R B3 -> 4R M1 -> 5R M1), unit-
|
|
@@ -544,6 +594,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
544
594
|
// session construction at exit — risks an ugly abort message, never data
|
|
545
595
|
// loss. Hanging forever is the alternative and is worse.
|
|
546
596
|
async shutdownAll() {
|
|
597
|
+
this.shuttingDown = true;
|
|
547
598
|
console.error('\n🧹 Cleaning up...');
|
|
548
599
|
try {
|
|
549
600
|
await this.coordinator?.shutdown(5000);
|
|
@@ -626,10 +677,206 @@ export class RAGKnowledgeGraphManager {
|
|
|
626
677
|
this.db.pragma('busy_timeout = 5000');
|
|
627
678
|
}
|
|
628
679
|
}
|
|
629
|
-
catch {
|
|
680
|
+
catch (e) {
|
|
681
|
+
// An I/O error here means the connection is pointing at a file that is no longer the one
|
|
682
|
+
// at the path (see guardSidecars). Retrying on the same connection cannot succeed.
|
|
683
|
+
if (isIoError(e))
|
|
684
|
+
this.reopenDb(`checkpoint-${e.code}`, true);
|
|
630
685
|
return false; // inside a transaction, or the file is locked — the next tick retries
|
|
631
686
|
}
|
|
632
687
|
}
|
|
688
|
+
// --- Sidecar guard (specs/changes/wal-sidecar-guard) -------------------------------------
|
|
689
|
+
// Measured 2026-09-25 (macOS + Google Drive): the sync client treated `-wal` as a synced item
|
|
690
|
+
// and swapped a new file in at the path while the engine held the old one open; every write
|
|
691
|
+
// then failed with SQLITE_IOERR until restart. Excluding the sidecars from sync takes them out
|
|
692
|
+
// of the client's hands, and it matches the WAL-exit contract: a WAL that never leaves this
|
|
693
|
+
// machine cannot be replayed onto another machine's main file. The attribute lives on the
|
|
694
|
+
// inode and SQLite recreates both files after the last connection closes, so it is set after
|
|
695
|
+
// every open and whenever a sidecar at the path is not the one recorded here.
|
|
696
|
+
walIno = null;
|
|
697
|
+
shmIno = null;
|
|
698
|
+
reopenCount = 0;
|
|
699
|
+
reopenTimes = [];
|
|
700
|
+
inFlight = 0; // tool calls running (reopen waits for them)
|
|
701
|
+
pendingReopen = null;
|
|
702
|
+
walMismatchTicks = 0; // a swap must be seen on 2 consecutive ticks
|
|
703
|
+
walWatchPausedUntil = 0; // storm brake for the inode watch
|
|
704
|
+
walWatchOff = false; // file system gives no inode (ino 0)
|
|
705
|
+
lastWalSize = 0;
|
|
706
|
+
dbDown = null;
|
|
707
|
+
shuttingDown = false;
|
|
708
|
+
// Set by main(): the full shutdown (closes the MCP transport first, then the engine). Without it
|
|
709
|
+
// the process would stay alive on stdin with no database.
|
|
710
|
+
onFatal = null;
|
|
711
|
+
guardSidecars() {
|
|
712
|
+
const wal = DB_FILE_PATH + '-wal', shm = DB_FILE_PATH + '-shm';
|
|
713
|
+
if (process.platform === 'darwin') {
|
|
714
|
+
for (const p of [wal, shm]) {
|
|
715
|
+
if (!fsSync.existsSync(p))
|
|
716
|
+
continue;
|
|
717
|
+
try {
|
|
718
|
+
execFileSync('/usr/bin/xattr', ['-w', SYNC_EXCLUDE_ATTR, '1', p], { stdio: 'ignore' });
|
|
719
|
+
}
|
|
720
|
+
catch (e) {
|
|
721
|
+
// Fail open: the attribute is protection, not a correctness condition.
|
|
722
|
+
console.error(`⚠️ sidecar sync-exclude failed: ${p}: ${e.message}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const w = fsSync.statSync(wal, { throwIfNoEntry: false, bigint: true });
|
|
727
|
+
const h = fsSync.statSync(shm, { throwIfNoEntry: false, bigint: true });
|
|
728
|
+
this.walIno = w ? w.ino : null;
|
|
729
|
+
this.shmIno = h ? h.ino : null;
|
|
730
|
+
if (w && w.ino === 0n && !this.walWatchOff) {
|
|
731
|
+
this.walWatchOff = true;
|
|
732
|
+
console.error('⚠️ file system reports inode 0 — sidecar swap watch disabled');
|
|
733
|
+
}
|
|
734
|
+
this.walMismatchTicks = 0;
|
|
735
|
+
}
|
|
736
|
+
// Runs on the WAL keeper tick. Returns true when it reopened.
|
|
737
|
+
walTick() {
|
|
738
|
+
if (this.shuttingDown)
|
|
739
|
+
return false;
|
|
740
|
+
if (!this.db)
|
|
741
|
+
return this.dbDown ? this.retryOpen() : false;
|
|
742
|
+
if (this.walWatchOff || Date.now() < this.walWatchPausedUntil)
|
|
743
|
+
return false;
|
|
744
|
+
const w = fsSync.statSync(DB_FILE_PATH + '-wal', { throwIfNoEntry: false, bigint: true });
|
|
745
|
+
const h = fsSync.statSync(DB_FILE_PATH + '-shm', { throwIfNoEntry: false, bigint: true });
|
|
746
|
+
if (this.walIno === null) {
|
|
747
|
+
// Nothing recorded yet (no write had created the file): record and mark it now.
|
|
748
|
+
if (w)
|
|
749
|
+
this.guardSidecars();
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
const walOk = !!w && w.ino === this.walIno;
|
|
753
|
+
const shmOk = this.shmIno === null || (!!h && h.ino === this.shmIno);
|
|
754
|
+
if (walOk && shmOk) {
|
|
755
|
+
this.walMismatchTicks = 0;
|
|
756
|
+
if (w)
|
|
757
|
+
this.lastWalSize = Number(w.size);
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
// Two consecutive ticks, so a value that flickers back within a second is not a swap.
|
|
761
|
+
if (++this.walMismatchTicks < 2)
|
|
762
|
+
return false;
|
|
763
|
+
const reason = !w ? 'wal-missing' : !walOk ? 'wal-inode-changed' : 'shm-inode-changed';
|
|
764
|
+
if (this.inFlight > 0) {
|
|
765
|
+
this.pendingReopen = reason;
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
return this.reopenDb(reason);
|
|
769
|
+
}
|
|
770
|
+
stormCount() {
|
|
771
|
+
const now = Date.now();
|
|
772
|
+
return this.reopenTimes.filter(t => now - t < REOPEN_STORM_WINDOW_MS).length;
|
|
773
|
+
}
|
|
774
|
+
// Close and reopen the connection with the same configuration. Migrations are not rerun.
|
|
775
|
+
// Returns false when there was nothing to reopen or the reopen failed (the engine then
|
|
776
|
+
// retries with backoff from the keeper tick and, after REOPEN_MAX_TRIES, exits so the host
|
|
777
|
+
// sees a failed server instead of a live one that answers every call with an error).
|
|
778
|
+
// force = an I/O error was seen on this handle: reopen even inside the storm window (holding a
|
|
779
|
+
// dead handle helps nobody). The storm brake is for the inode watch.
|
|
780
|
+
reopenDb(reason, force = false) {
|
|
781
|
+
if (this.shuttingDown || !this.db)
|
|
782
|
+
return false;
|
|
783
|
+
if (!force && this.stormCount() >= REOPEN_STORM_MAX) {
|
|
784
|
+
// Keep the connection; pause the inode watch so a flickering file system cannot make
|
|
785
|
+
// this a once-a-second close/open loop. An IOERR path still gets its error.
|
|
786
|
+
this.walWatchPausedUntil = Date.now() + WAL_WATCH_PAUSE_MS;
|
|
787
|
+
console.error(`⚠️ reopen storm (${REOPEN_STORM_MAX} in ${REOPEN_STORM_WINDOW_MS / 1000}s) — watch paused, reason=${reason}`);
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
const old = this.db;
|
|
791
|
+
// Salvage: frames committed to the old WAL after the last checkpoint (≤1 s) go to the main
|
|
792
|
+
// file if the old descriptor is still readable. An I/O error here just means they are gone.
|
|
793
|
+
// busy_timeout 0: another engine's read snapshot must not stall this for 5 s.
|
|
794
|
+
try {
|
|
795
|
+
old.pragma('busy_timeout = 0');
|
|
796
|
+
old.pragma('wal_checkpoint(TRUNCATE)');
|
|
797
|
+
}
|
|
798
|
+
catch { /* old file unreadable */ }
|
|
799
|
+
try {
|
|
800
|
+
old.close();
|
|
801
|
+
}
|
|
802
|
+
catch { /* the handle may already be unusable */ }
|
|
803
|
+
this.db = null;
|
|
804
|
+
this.reopenTimes.push(Date.now());
|
|
805
|
+
return this.openAfterClose(reason);
|
|
806
|
+
}
|
|
807
|
+
openAfterClose(reason) {
|
|
808
|
+
try {
|
|
809
|
+
this.openConnection();
|
|
810
|
+
this.guardSidecars();
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
try {
|
|
814
|
+
this.db?.close();
|
|
815
|
+
}
|
|
816
|
+
catch { /* half-open */ }
|
|
817
|
+
this.db = null;
|
|
818
|
+
const tries = (this.dbDown?.tries ?? 0) + 1;
|
|
819
|
+
const delay = Math.min(1000 * 2 ** (tries - 1), 60_000);
|
|
820
|
+
this.dbDown = { reason, tries, nextAt: Date.now() + delay };
|
|
821
|
+
console.error(`❌ db reopen failed (reason=${reason}, try=${tries}): ${e.message}`);
|
|
822
|
+
if (tries >= REOPEN_MAX_TRIES) {
|
|
823
|
+
console.error('❌ database cannot be reopened — exiting so the host shows a failed server');
|
|
824
|
+
process.exitCode = 1;
|
|
825
|
+
if (this.onFatal)
|
|
826
|
+
this.onFatal();
|
|
827
|
+
else
|
|
828
|
+
void this.shutdownAll().finally(() => process.exit(process.exitCode ?? 1));
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
this.dbDown = null;
|
|
833
|
+
this.pendingReopen = null;
|
|
834
|
+
this.reopenCount++;
|
|
835
|
+
console.error(`🔁 db reopened (reason=${reason}, n=${this.reopenCount}, walsz-before=${this.lastWalSize})`);
|
|
836
|
+
// A reconciliation cut short by the old handle is 'pending', not failed — run it again.
|
|
837
|
+
if (this.coordinator?.reconState === 'pending')
|
|
838
|
+
void this.startReconciliation().catch(() => { });
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
retryOpen() {
|
|
842
|
+
if (this.shuttingDown)
|
|
843
|
+
return false;
|
|
844
|
+
if (!this.dbDown || Date.now() < this.dbDown.nextAt)
|
|
845
|
+
return false;
|
|
846
|
+
return this.openAfterClose(`retry-${this.dbDown.reason}`);
|
|
847
|
+
}
|
|
848
|
+
// Tool-call wrapper: an SQLITE_IOERR* reopens the connection. The call is retried once only
|
|
849
|
+
// when the tool is idempotent — a failed transaction is rolled back whole, but a tool can
|
|
850
|
+
// span several transactions, so a non-idempotent one gets the error back (after the reopen)
|
|
851
|
+
// and the caller decides. Too many reopens in a short window → no retry, surface the error.
|
|
852
|
+
async runWithIoRecovery(tool, fn) {
|
|
853
|
+
if (!this.db && this.dbDown)
|
|
854
|
+
this.retryOpen();
|
|
855
|
+
const handle = this.db;
|
|
856
|
+
this.inFlight++;
|
|
857
|
+
try {
|
|
858
|
+
try {
|
|
859
|
+
return await fn();
|
|
860
|
+
}
|
|
861
|
+
catch (e) {
|
|
862
|
+
if (!isIoError(e))
|
|
863
|
+
throw e;
|
|
864
|
+
const storm = this.stormCount() >= REOPEN_STORM_MAX;
|
|
865
|
+
// Several calls hit by one swap reopen once: if another call already replaced the
|
|
866
|
+
// handle, just use the new one.
|
|
867
|
+
const reopened = (this.db !== null && this.db !== handle)
|
|
868
|
+
|| this.reopenDb(`tool-${tool}-${e.code}`, true);
|
|
869
|
+
if (storm || !reopened || !IDEMPOTENT_TOOLS.has(tool))
|
|
870
|
+
throw e;
|
|
871
|
+
return await fn();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
this.inFlight--;
|
|
876
|
+
if (this.inFlight === 0 && this.pendingReopen)
|
|
877
|
+
this.reopenDb(this.pendingReopen);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
633
880
|
walTimer = null;
|
|
634
881
|
walSoon = null;
|
|
635
882
|
// Every write path ends up here without having to be listed: the periodic tick looks at
|
|
@@ -638,7 +885,15 @@ export class RAGKnowledgeGraphManager {
|
|
|
638
885
|
startWalKeeper(tickMs = 1000) {
|
|
639
886
|
if (this.walTimer)
|
|
640
887
|
return;
|
|
641
|
-
this.walTimer = setInterval(() => {
|
|
888
|
+
this.walTimer = setInterval(() => {
|
|
889
|
+
try {
|
|
890
|
+
this.walTick();
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
console.error(`⚠️ wal tick: ${e.message}`);
|
|
894
|
+
}
|
|
895
|
+
this.checkpointWal();
|
|
896
|
+
}, tickMs);
|
|
642
897
|
this.walTimer.unref();
|
|
643
898
|
}
|
|
644
899
|
noteActivity(delayMs = 200) {
|
|
@@ -648,6 +903,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
648
903
|
this.walSoon.unref();
|
|
649
904
|
}
|
|
650
905
|
cleanup() {
|
|
906
|
+
this.shuttingDown = true;
|
|
651
907
|
if (this.walTimer) {
|
|
652
908
|
clearInterval(this.walTimer);
|
|
653
909
|
this.walTimer = null;
|
|
@@ -3311,6 +3567,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
3311
3567
|
throw new Error('Database not initialized');
|
|
3312
3568
|
if (!this.encoding)
|
|
3313
3569
|
throw new Error('Tokenizer not initialized');
|
|
3570
|
+
// search-fusion-rrf: summary mode is read ONCE, at call entry (before any await), and used for the
|
|
3571
|
+
// whole call. Opt-in since this change (was opt-out via =off in v5..6.3.2).
|
|
3572
|
+
const summariesOn = process.env.RAG_MEMORY_SEARCH_SUMMARIES === 'on';
|
|
3314
3573
|
console.error(`🔍 Enhanced hybrid search: "${query}"`);
|
|
3315
3574
|
// Parity with searchNodes (beta 1R supplement): an unsearchable query gets
|
|
3316
3575
|
// an explicit warning instead of a silent empty envelope.
|
|
@@ -3374,6 +3633,11 @@ export class RAGKnowledgeGraphManager {
|
|
|
3374
3633
|
}
|
|
3375
3634
|
catch (embErr) {
|
|
3376
3635
|
vectorDegraded = true;
|
|
3636
|
+
// search-fusion-rrf (D5): a failure on a LATER cross-lingual variant used to leave the earlier
|
|
3637
|
+
// variants' vector candidates in resultMap while the envelope said 'fts-only'. Drop them so the
|
|
3638
|
+
// degraded call really is FTS-only (no vector rank, no summary, no graph) in every mode.
|
|
3639
|
+
resultMap.clear();
|
|
3640
|
+
primaryQueryEmbedding = null;
|
|
3377
3641
|
// 'inference_error' (not 'model_not_ready'): model_state may still read
|
|
3378
3642
|
// 'ready' here — a contradictory reason pair confused callers (beta B6).
|
|
3379
3643
|
degradationReason = this.degradationReason() ?? 'inference_error';
|
|
@@ -3381,6 +3645,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
3381
3645
|
}
|
|
3382
3646
|
}
|
|
3383
3647
|
const vectorResults = Array.from(resultMap.values()).sort((a, b) => a.distance - b.distance);
|
|
3648
|
+
// search-fusion-rrf: vector rank (1-based, best distance across variants) for rank fusion.
|
|
3649
|
+
// Taken here, before FTS5-only candidates are appended to vectorResults below.
|
|
3650
|
+
const vectorRankOf = new Map(vectorResults.map((r, i) => [r.chunk_id, i + 1]));
|
|
3651
|
+
// Default ranking = reciprocal rank fusion of the vector list and the FTS5 list (k=60).
|
|
3652
|
+
// The legacy formula stays for the opt-in diagnostics (summaries on, graph re-ranker on).
|
|
3653
|
+
const rankFusion = !useGraph && !summariesOn;
|
|
3384
3654
|
// FTS5 full-text search as additional signal (Reciprocal Rank Fusion)
|
|
3385
3655
|
const ftsBoostMap = new Map();
|
|
3386
3656
|
try {
|
|
@@ -3569,12 +3839,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
3569
3839
|
graphBoost += Math.min(entityBoost, 0.4);
|
|
3570
3840
|
}
|
|
3571
3841
|
// Generate semantic summary (skip when degraded — no embeddings available).
|
|
3572
|
-
//
|
|
3573
|
-
//
|
|
3574
|
-
// measured
|
|
3575
|
-
//
|
|
3842
|
+
// search-fusion-rrf: the summary path is OPT-IN (RAG_MEMORY_SEARCH_SUMMARIES=on). It embeds
|
|
3843
|
+
// every sentence fragment of every candidate one at a time (~400 inferences per search on a
|
|
3844
|
+
// real corpus, median 121 s measured 2026-10-03) and its unbounded context boost let
|
|
3845
|
+
// vector-0 chunks take rank 1 (known-item hit@1 9/10 off vs 4/10 on). Default = preview slices.
|
|
3576
3846
|
let summary, keyHighlight, relevanceScore;
|
|
3577
|
-
if (vectorDegraded || !primaryQueryEmbedding ||
|
|
3847
|
+
if (vectorDegraded || !primaryQueryEmbedding || !summariesOn) {
|
|
3578
3848
|
keyHighlight = result.text.slice(0, 150);
|
|
3579
3849
|
summary = result.text.slice(0, 300);
|
|
3580
3850
|
relevanceScore = 0;
|
|
@@ -3584,8 +3854,11 @@ export class RAGKnowledgeGraphManager {
|
|
|
3584
3854
|
));
|
|
3585
3855
|
}
|
|
3586
3856
|
const vectorSimilarity = Math.max(0, 1 - result.distance / 2);
|
|
3587
|
-
const ftsBoost = ftsBoostMap.get(result.chunk_id) || 0;
|
|
3588
|
-
const
|
|
3857
|
+
const ftsBoost = ftsBoostMap.get(result.chunk_id) || 0; // = 1/(60 + FTS rank), 0 if not an FTS hit
|
|
3858
|
+
const vectorRank = vectorRankOf.get(result.chunk_id);
|
|
3859
|
+
const finalScore = rankFusion
|
|
3860
|
+
? (vectorRank !== undefined ? 1 / (60 + vectorRank) : 0) + ftsBoost
|
|
3861
|
+
: Math.max(vectorSimilarity, relevanceScore) + graphBoost + ftsBoost;
|
|
3589
3862
|
// Determine document title and source ID
|
|
3590
3863
|
let documentTitle;
|
|
3591
3864
|
let sourceId;
|
|
@@ -4127,116 +4400,119 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4127
4400
|
// Validate arguments using our structured schema
|
|
4128
4401
|
const validatedArgs = validateToolArgs(name, args);
|
|
4129
4402
|
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
|
-
|
|
4403
|
+
// An SQLITE_IOERR* reopens the connection; idempotent tools are retried once (wal-sidecar-guard).
|
|
4404
|
+
return await ragKgManager.runWithIoRecovery(name, async () => {
|
|
4405
|
+
switch (name) {
|
|
4406
|
+
// Original MCP tools
|
|
4407
|
+
case "createEntities":
|
|
4408
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.createEntities(validatedArgs.entities), null, 2) }] };
|
|
4409
|
+
case "createRelations":
|
|
4410
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.createRelations(validatedArgs.relations), null, 2) }] };
|
|
4411
|
+
case "addObservations":
|
|
4412
|
+
// v13: status·sources 를 그대로 넘긴다. 여기서 떨어뜨리면 스키마가 받아도
|
|
4413
|
+
// 엔진에 도달하지 않아 provenance 가 조용히 사라진다.
|
|
4414
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.addObservations(validatedArgs.observations), null, 2) }] };
|
|
4415
|
+
// v13 observation lifecycle (spec §6.1 / §6.2)
|
|
4416
|
+
case "correctObservation":
|
|
4417
|
+
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) }] };
|
|
4418
|
+
case "retractObservation":
|
|
4419
|
+
await ragKgManager.retractObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4420
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'retracted' }, null, 2) }] };
|
|
4421
|
+
case "restoreObservation":
|
|
4422
|
+
await ragKgManager.restoreObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4423
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'active' }, null, 2) }] };
|
|
4424
|
+
case "approveObservation":
|
|
4425
|
+
await ragKgManager.approveObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4426
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'active' }, null, 2) }] };
|
|
4427
|
+
case "declineObservation":
|
|
4428
|
+
await ragKgManager.declineObservation(validatedArgs.observation_id, validatedArgs.reason);
|
|
4429
|
+
return { content: [{ type: "text", text: JSON.stringify({ observation_id: validatedArgs.observation_id, status: 'retracted' }, null, 2) }] };
|
|
4430
|
+
case "purgeObservation":
|
|
4431
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.purgeObservation(validatedArgs.observation_id, validatedArgs.confirm), null, 2) }] };
|
|
4432
|
+
case "getObservationHistory":
|
|
4433
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getObservationHistory({
|
|
4434
|
+
entity_name: validatedArgs.entity_name,
|
|
4435
|
+
observation_id: validatedArgs.observation_id,
|
|
4436
|
+
root_id: validatedArgs.root_id,
|
|
4437
|
+
}), null, 2) }] };
|
|
4438
|
+
case "deleteEntities":
|
|
4439
|
+
await ragKgManager.deleteEntities(validatedArgs.entityNames);
|
|
4440
|
+
return { content: [{ type: "text", text: "Entities deleted successfully" }] };
|
|
4441
|
+
case "deleteObservations":
|
|
4442
|
+
// v3.6 (spec §5c, breaking): structured per-entity results replace the
|
|
4443
|
+
// bare success string — mixed embedded/queued/no-op states are visible.
|
|
4444
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.deleteObservations(validatedArgs.deletions), null, 2) }] };
|
|
4445
|
+
case "deleteRelations":
|
|
4446
|
+
await ragKgManager.deleteRelations(validatedArgs.relations);
|
|
4447
|
+
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
|
4448
|
+
case "updateRelations":
|
|
4449
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.updateRelations(validatedArgs.updates), null, 2) }] };
|
|
4450
|
+
case "readGraph":
|
|
4451
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.readGraph(), null, 2) }] };
|
|
4452
|
+
case "searchNodes":
|
|
4453
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10, validatedArgs.since, validatedArgs.until), null, 2) }] };
|
|
4454
|
+
case "openNodes":
|
|
4455
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
|
|
4456
|
+
case "getNeighbors":
|
|
4457
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getNeighbors(validatedArgs.entityNames, validatedArgs.depth || 1, validatedArgs.relationType), null, 2) }] };
|
|
4458
|
+
// New RAG tools
|
|
4459
|
+
case "storeDocument":
|
|
4460
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.storeDocument(validatedArgs.id, validatedArgs.content, validatedArgs.metadata || {}), null, 2) }] };
|
|
4461
|
+
case "chunkDocument":
|
|
4462
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.chunkDocument(validatedArgs.documentId, { maxTokens: validatedArgs.maxTokens, overlap: validatedArgs.overlap }), null, 2) }] };
|
|
4463
|
+
case "embedChunks":
|
|
4464
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedChunks(validatedArgs.documentId), null, 2) }] };
|
|
4465
|
+
case "extractTerms":
|
|
4466
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.extractTerms(validatedArgs.documentId, { minLength: validatedArgs.minLength, includeCapitalized: validatedArgs.includeCapitalized, customPatterns: validatedArgs.customPatterns }), null, 2) }] };
|
|
4467
|
+
case "linkEntitiesToDocument":
|
|
4468
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.linkEntitiesToDocument(validatedArgs.documentId, validatedArgs.entityNames), null, 2) }] };
|
|
4469
|
+
case "hybridSearch":
|
|
4470
|
+
const limit = typeof validatedArgs.limit === 'number' ? validatedArgs.limit : 5;
|
|
4471
|
+
// v5.3.0: graph is opt-in — only an explicit true enables the re-ranker (schema default false).
|
|
4472
|
+
const useGraph = validatedArgs.useGraph === true;
|
|
4473
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.hybridSearch(validatedArgs.query, limit, useGraph), null, 2) }] };
|
|
4474
|
+
case "getDetailedContext":
|
|
4475
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getDetailedContext(validatedArgs.chunkId, validatedArgs.includeSurrounding !== false), null, 2) }] };
|
|
4476
|
+
case "getKnowledgeGraphStats":
|
|
4477
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getKnowledgeGraphStats(), null, 2) }] };
|
|
4478
|
+
case "deleteDocuments":
|
|
4479
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.deleteDocuments(validatedArgs.documentIds), null, 2) }] };
|
|
4480
|
+
case "listDocuments":
|
|
4481
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.listDocuments(validatedArgs.includeMetadata !== false), null, 2) }] };
|
|
4482
|
+
case "syncDocumentFromFile":
|
|
4483
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.syncDocumentFromFile(validatedArgs.path, validatedArgs.documentId, {
|
|
4484
|
+
metadata: validatedArgs.metadata,
|
|
4485
|
+
content: validatedArgs.content,
|
|
4486
|
+
excludePattern: validatedArgs.excludePattern,
|
|
4487
|
+
entityNames: validatedArgs.entityNames,
|
|
4488
|
+
chunkParams: validatedArgs.chunkParams,
|
|
4489
|
+
}), null, 2) }] };
|
|
4490
|
+
// NEW: Entity embedding tools
|
|
4491
|
+
case "embedAllEntities":
|
|
4492
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedAllEntities(), null, 2) }] };
|
|
4493
|
+
// NEW: Export/Import tools
|
|
4494
|
+
case "exportGraph":
|
|
4495
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.exportGraph(), null, 2) }] };
|
|
4496
|
+
case "importGraph":
|
|
4497
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.importGraph(validatedArgs.data, { merge: validatedArgs.merge !== false }), null, 2) }] };
|
|
4498
|
+
// NEW: Migration tools
|
|
4499
|
+
case "getMigrationStatus":
|
|
4500
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getMigrationStatus(), null, 2) }] };
|
|
4501
|
+
case "runMigrations":
|
|
4502
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.runMigrations(), null, 2) }] };
|
|
4503
|
+
case "rollbackMigration":
|
|
4504
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.rollbackMigration(validatedArgs.targetVersion), null, 2) }] };
|
|
4505
|
+
// Graph Analytics tools (graphology)
|
|
4506
|
+
case "getGraphMetrics":
|
|
4507
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getGraphMetrics(validatedArgs.entityNames, validatedArgs.metrics, validatedArgs.limit || 10), null, 2) }] };
|
|
4508
|
+
case "detectCommunities":
|
|
4509
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.detectCommunities(validatedArgs.resolution || 1.0), null, 2) }] };
|
|
4510
|
+
case "analyzeGraphStructure":
|
|
4511
|
+
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.analyzeGraphStructure(), null, 2) }] };
|
|
4512
|
+
default:
|
|
4513
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
4240
4516
|
}
|
|
4241
4517
|
catch (error) {
|
|
4242
4518
|
// v3.6 (spec §5c): machine-distinguishable failures. Embedding-gate errors
|
|
@@ -4301,6 +4577,7 @@ async function main() {
|
|
|
4301
4577
|
if (shuttingDown)
|
|
4302
4578
|
return;
|
|
4303
4579
|
shuttingDown = true;
|
|
4580
|
+
ragKgManager.shuttingDown = true; // no reopen inside the exit window (wal-sidecar-guard)
|
|
4304
4581
|
// Synchronously, before anything is awaited: a host may SIGKILL before the settle
|
|
4305
4582
|
// sequence below completes (codex: ~185 ms after SIGTERM).
|
|
4306
4583
|
ragKgManager.checkpointWal();
|
|
@@ -4325,6 +4602,7 @@ async function main() {
|
|
|
4325
4602
|
// Ctrl+Break; registering them is harmless where they never fire.
|
|
4326
4603
|
process.on('SIGHUP', shutdown);
|
|
4327
4604
|
process.on('SIGBREAK', shutdown);
|
|
4605
|
+
ragKgManager.onFatal = shutdown; // a database that cannot be reopened ends the server (wal-sidecar-guard)
|
|
4328
4606
|
ragKgManager.startWalKeeper();
|
|
4329
4607
|
process.on('exit', () => { try {
|
|
4330
4608
|
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}`);
|
|
@@ -277,7 +277,7 @@ export const addObservationsTool = {
|
|
|
277
277
|
};
|
|
278
278
|
// === HYBRID SEARCH TOOL ===
|
|
279
279
|
const hybridSearchCapability = {
|
|
280
|
-
description: 'Perform
|
|
280
|
+
description: 'Perform hybrid search over vector and FTS5 candidates with default reciprocal rank fusion and an optional legacy graph re-ranker',
|
|
281
281
|
parameters: {
|
|
282
282
|
type: 'object',
|
|
283
283
|
properties: {
|
|
@@ -300,7 +300,8 @@ const hybridSearchCapability = {
|
|
|
300
300
|
},
|
|
301
301
|
};
|
|
302
302
|
const hybridSearchDescription = () => `<description>
|
|
303
|
-
Hybrid document search: vector similarity + FTS5 BM25 over document chunks,
|
|
303
|
+
Hybrid document search: vector similarity + FTS5 BM25 over document chunks, fused by rank (reciprocal rank fusion,
|
|
304
|
+
k=60, so an exact identifier that only FTS5 finds can still enter the returned top results), with an **opt-in** knowledge-graph re-ranker.
|
|
304
305
|
Default (useGraph false) is the harm-reduced default for finding a fact you know exists (known-item retrieval).
|
|
305
306
|
useGraph: true is a legacy/experimental re-ranker kept for backward compatibility and controlled evaluation — it does not
|
|
306
307
|
add candidates, it only re-orders the vector/FTS pool, and measured on three real corpora (2026-08-17) it pushed the exact
|
|
@@ -310,14 +311,14 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
310
311
|
<importantNotes>
|
|
311
312
|
- (!important!) **Graph re-ranking is opt-in (default false since v5.3.0)** — it re-orders candidates by matched/connected entity links, which is measured to hurt known-item retrieval; enable it only for backward compatibility or controlled evaluation
|
|
312
313
|
- (!important!) Graph re-ranking does not generate candidates — for "what is connected to X" use openNodes -> getNeighbors
|
|
313
|
-
- (!important!)
|
|
314
|
+
- (!important!) relevance_score is a RANK score on the default call: 1/(60 + vector rank) + 1/(60 + FTS rank), at most 2/61; it is not a similarity or probability and is only comparable within one call (vector_similarity and fts_boost are reported alongside)
|
|
314
315
|
- (!important!) **Best results when knowledge graph is well-populated** with entities and relationships
|
|
315
316
|
</importantNotes>
|
|
316
317
|
|
|
317
318
|
<whenToUseThisTool>
|
|
318
319
|
- When you need comprehensive search across documents and knowledge
|
|
319
320
|
- For complex queries requiring conceptual understanding
|
|
320
|
-
-
|
|
321
|
+
- Exact identifiers (file names, setting keys, function names, hashes) as well as natural-language questions
|
|
321
322
|
- **Before making decisions** - to gather all relevant information
|
|
322
323
|
- When researching topics that span multiple domains
|
|
323
324
|
- For discovery of implicit connections and patterns
|
|
@@ -325,7 +326,8 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
325
326
|
|
|
326
327
|
<features>
|
|
327
328
|
- Vector similarity search using sentence transformers
|
|
328
|
-
-
|
|
329
|
+
- Rank fusion of vector and FTS5 candidates: an exact term found only by FTS5 can enter the returned top results even when vector candidates are present
|
|
330
|
+
- Optional legacy graph re-ranker (re-orders only; no traversal — use openNodes -> getNeighbors)
|
|
329
331
|
- Hybrid scoring combining multiple relevance signals
|
|
330
332
|
- Entity association highlighting
|
|
331
333
|
- Configurable result limits and graph usage
|
|
@@ -333,7 +335,7 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
333
335
|
</features>
|
|
334
336
|
|
|
335
337
|
<bestPractices>
|
|
336
|
-
-
|
|
338
|
+
- Natural-language questions and exact identifiers both work; pass the identifier itself when you know it
|
|
337
339
|
- Keep graph re-ranking off for "find the fact I know exists"; for "what is connected to this" use openNodes -> getNeighbors, not useGraph
|
|
338
340
|
- Start with broader queries, then narrow down based on results
|
|
339
341
|
- Review entity associations to understand why results were selected
|
|
@@ -342,7 +344,7 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
342
344
|
</bestPractices>
|
|
343
345
|
|
|
344
346
|
<parameters>
|
|
345
|
-
- query:
|
|
347
|
+
- query: Search query — a natural-language question or an exact term/identifier (string, required)
|
|
346
348
|
- limit: Maximum results to return, default 5 (number, optional)
|
|
347
349
|
- useGraph: Enable knowledge graph re-ranking, default false / opt-in (boolean, optional)
|
|
348
350
|
</parameters>
|
package/docs/UPDATING.md
CHANGED
|
@@ -108,6 +108,52 @@ path and holder pid (e.g. `.download-<key>.lock`). Verify the holder process
|
|
|
108
108
|
is genuinely gone or hung (`ps -p <pid>`), then remove the lock file manually;
|
|
109
109
|
the next start becomes a clean download owner.
|
|
110
110
|
|
|
111
|
+
## v6.4.0 (schema v14, unchanged): `hybridSearch` default = rank fusion, summaries opt-in
|
|
112
|
+
|
|
113
|
+
(Release candidate `6.4.0-rc.1` on the `next` dist-tag first; `latest` stays 6.3.2 until the published artifact
|
|
114
|
+
passes a canary against a real project database.)
|
|
115
|
+
|
|
116
|
+
**What changed.** (1) The per-result summary path runs only with `RAG_MEMORY_SEARCH_SUMMARIES=on` (it used to
|
|
117
|
+
run unless the variable was `off`; `off` still means off). (2) With summaries off and `useGraph: false` — the
|
|
118
|
+
default call — results are ranked by reciprocal rank fusion of the vector list and the FTS5 list:
|
|
119
|
+
`relevance_score = 1/(60 + vector rank) + 1/(60 + FTS rank)`, a list a chunk is absent from contributes 0.
|
|
120
|
+
Healthy-path candidate gathering and depth are unchanged. If vector search fails after an earlier query variant
|
|
121
|
+
succeeded, the call now discards those partial vector candidates and returns a true FTS-only result (the previous
|
|
122
|
+
version could keep them while reporting `fts-only`). `summaries=on` or `useGraph: true` keep the 6.3.2 formula
|
|
123
|
+
(`max(vector_similarity, summary relevance) + graph boost + FTS term`) unchanged.
|
|
124
|
+
|
|
125
|
+
**Why.** Measured 2026-10-03 on the frozen eval snapshots (spec `specs/changes/search-fusion-rrf/proposal.md`):
|
|
126
|
+
the summary path embedded ~400 sentence fragments per search (median 121 s) and its unbounded context boost let
|
|
127
|
+
vector-0 chunks take rank 1 (known-item hit@1 9/10 with summaries off vs 4/10 on). With summaries off, a chunk
|
|
128
|
+
found only by FTS scored at most 0.0164 against ~0.4-0.6 for every vector candidate, so exact-identifier
|
|
129
|
+
queries surfaced their target in the top 10 for 7/20 queries although FTS ranked it first for 17/20. Rank fusion:
|
|
130
|
+
18/20, and worse than the old formula on 0 of 203 queries across three corpora.
|
|
131
|
+
|
|
132
|
+
**Query input (same release).** With the default model (bge-m3) queries are embedded as raw text; a custom
|
|
133
|
+
`EMBEDDING_MODEL` keeps the previous query instruction (the evidence below is for bge-m3 only). Up to 6.3.2 the loader prepended the bge-v1.5
|
|
134
|
+
instruction `Represent this sentence for searching relevant passages: ` to every query (hybridSearch, searchNodes,
|
|
135
|
+
graph seeds) while stored vectors were embedded without it; the bge-m3 model card says the model needs no query
|
|
136
|
+
instruction. Measured on the frozen snapshots: hybridSearch paraphrase hit@10 7 -> 9 / 20 and MRR 0.217 -> 0.275,
|
|
137
|
+
identifier hit@1 7 -> 8, known-item unchanged on hub / uap and hit@1 +1 on hal; searchNodes known-item (40 entities per
|
|
138
|
+
corpus) hit@10 unchanged, hit@1 -1 / -1 / +1. No re-index: only query vectors change; rolling back means a restart.
|
|
139
|
+
`server_meta.query_prefix_version` is a diagnostic marker written at init (2 = raw queries with the default model,
|
|
140
|
+
1 = instruction); it does not prove what the last query used.
|
|
141
|
+
|
|
142
|
+
**What a caller notices.** On the default call `relevance_score` is a rank score — at most 2/61 ≈ 0.0328, no lower
|
|
143
|
+
bound (a chunk in one list at rank r gets 1/(60+r)), comparable only within one call; do not threshold it against
|
|
144
|
+
old values or across queries/modes. `vector_similarity` and `fts_boost` keep their meaning. `content_summary`
|
|
145
|
+
/ `key_highlight` are preview slices unless summaries are turned on. The fragment-summary work is gone from default
|
|
146
|
+
searches: measured query-cached medians were 20 / 10 / 6 ms on the frozen hub / uap / hal runs; with the query
|
|
147
|
+
embedding cache cleared before every call (one Mac, final build) medians were about 220 / 228 /
|
|
148
|
+
203 ms (max 888 ms). Other machines and corpora are unmeasured — this is not a universal millisecond guarantee. With
|
|
149
|
+
`useGraph: true` and the variable unset, summaries are now off; set `RAG_MEMORY_SEARCH_SUMMARIES=on` to reproduce
|
|
150
|
+
the previous summary-enabled graph path.
|
|
151
|
+
|
|
152
|
+
**Fleet rollout.** No migration. Recommended like v5.3.0: publish an rc on the `next` dist-tag first, run it
|
|
153
|
+
against a real project database (identifier query + known-item probe), then promote to `latest`.
|
|
154
|
+
Regression locks: `test/search-fusion-rrf.test.mjs`, `test/search-summaries-off.test.mjs`, and
|
|
155
|
+
`test/graph-context-explain.test.mjs` (legacy path byte-identical with summaries on).
|
|
156
|
+
|
|
111
157
|
## v5.3.0 (schema v14, unchanged): `hybridSearch` graph re-ranking is opt-in
|
|
112
158
|
|
|
113
159
|
**What changed.** `useGraph` defaults to `false` (was `true`) — in the manager signature, the tool JSON
|
|
@@ -207,8 +253,10 @@ the real v14 (this is exactly how code-v8 never ran in production).
|
|
|
207
253
|
**Diagnostic env (v5)**: `RAG_MEMORY_SEARCH_SUMMARIES=off` disables the per-result
|
|
208
254
|
sentence-similarity summaries in `hybridSearch` (which embed every sentence of every
|
|
209
255
|
candidate — 100+ inferences, 90-120s cold per search, measured). Off = preview-slice
|
|
210
|
-
summaries,
|
|
211
|
-
|
|
256
|
+
summaries, the internal summary relevance is 0 (the response's `relevance_score` is still the final score) and
|
|
257
|
+
ranking rests on vector similarity + boosts. **Superseded by the next
|
|
258
|
+
release: summaries are off by default and opt-in with `=on`** (see the section above). The 3-arm release
|
|
259
|
+
harness sets this uniformly across all arms.
|
|
212
260
|
|
|
213
261
|
## v3.6 breaking response changes
|
|
214
262
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0-rc.1",
|
|
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/search-fusion-rrf.test.mjs && node test/query-input.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
|
},
|