rag-memory-epf-mcp 6.4.1 → 6.4.3
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 +37 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +234 -35
- package/dist/src/backfillCoordinator.d.ts +10 -0
- package/dist/src/backfillCoordinator.js +129 -40
- package/dist/src/observations/projection.js +4 -3
- package/dist/src/vecSlots.d.ts +51 -0
- package/dist/src/vecSlots.js +323 -0
- package/docs/UPDATING.md +17 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -154,6 +154,43 @@ storeDocument(id, content, metadata)
|
|
|
154
154
|
|
|
155
155
|
## Changelog
|
|
156
156
|
|
|
157
|
+
### v6.4.3 (2026-11-02)
|
|
158
|
+
|
|
159
|
+
- **Fixed — re-embedding no longer leaks vector slots.** sqlite-vec puts a new row only into a free slot of the latest
|
|
160
|
+
1,024-slot chunk, so the old DELETE + INSERT of the same rowid left a hole whenever the row sat in an older chunk (one
|
|
161
|
+
project DB reached 84 chunks for 5,578 vectors, 93.5% empty, 337 of 439 MB). Same-rowid re-embeds (document chunks,
|
|
162
|
+
KG chunks, bulk `embedChunks`, entities) now rewrite the vector in place; entity re-embeds keep their rowid.
|
|
163
|
+
- **Added — idle verified rebuild of the vector tables.** When the engine is idle (no tool call, backfill or
|
|
164
|
+
reconciliation; 60 s quiet; checked at most every 10 min) and a table has at least 8 reclaimable chunks that outweigh
|
|
165
|
+
its live vectors, or holds damaged slots, it is rebuilt in one transaction keeping every searchable rowid and its
|
|
166
|
+
exact vector bytes (verified before commit; rolled back otherwise). Skipped at once if another connection holds the
|
|
167
|
+
write lock. Automatic only up to 20,000 live vectors per table (measured on one Mac: 5,600 = 0.44 s, 20,000 =
|
|
168
|
+
1.24 s and +267 MB transient). No VACUUM: the file stops growing; it shrinks only through a VACUUM.
|
|
169
|
+
- **Fixed — coverage counts only searchable vectors.** A row whose slot validity bit is 0 (invisible to search) was
|
|
170
|
+
counted as embedded; it is now missing and is repaired: writes and deletes that meet such a slot, or a slot shared by
|
|
171
|
+
two rowids, rebuild the table first in the same transaction, then the backfill re-embeds at the original rowid.
|
|
172
|
+
An unresolvable slot position is reported as an error (`missing: null`), never as a count.
|
|
173
|
+
- **Added — `getKnowledgeGraphStats().server.vector_slots`**: per table slots, valid, validity-0, shared, empty,
|
|
174
|
+
reclaimable chunks, whether a rebuild is due and why, and this process's last attempt.
|
|
175
|
+
- **Added — write-seq signal** (`[rag-memory] write-seq N` on stderr when the engine's own connection changed rows),
|
|
176
|
+
used by the framework launcher to record a DB handoff only after the engine wrote.
|
|
177
|
+
- **Changed — an entity metadata row whose vector row is gone is kept unstamped (missing) instead of deleted**, so the
|
|
178
|
+
backfill re-inserts at the same rowid; new entity rowids are allocated past both tables.
|
|
179
|
+
- Not measured: Windows, Drive-synced DBs under two machines, tables above 20,000 vectors.
|
|
180
|
+
|
|
181
|
+
### v6.4.2 (2026-10-31)
|
|
182
|
+
|
|
183
|
+
- **Fixed — the engine no longer keeps the peak memory of its largest embedding.** The ONNX session is opened with
|
|
184
|
+
the CPU memory arena off. With it on, one long input (12,000 characters, 5,774 tokens) left the process at about
|
|
185
|
+
8 GB until it exited (1.4 GB after model load), and every MCP client runs its own engine. With it off the same
|
|
186
|
+
input left 1.5-2.4 GB in a probe and 2.7-3.2 GB in the full engine (one Mac, one run each). Vectors are
|
|
187
|
+
byte-identical with the arena on and off. The transient peak while a long input is being embedded is not lowered.
|
|
188
|
+
- **Changed — `@huggingface/transformers` 4.3.2** (was `^3.5.1`; onnxruntime-node 1.30.0). The model cache layout is
|
|
189
|
+
unchanged and an existing cache loads without a re-download. Vectors differ from 3.x only in the last bits (cosine
|
|
190
|
+
0.9999995 or higher on 5 inputs); the embedding profile is unchanged, so nothing is re-embedded. Measured on one
|
|
191
|
+
Mac: a 30-character query 128 ms -> 33 ms, a 484-token chunk 739 ms -> 639 ms (3.8.1 with the arena on vs 4.3.2
|
|
192
|
+
with it off). Not measured: fixed-query ranking of existing document vectors against new query vectors, Windows.
|
|
193
|
+
|
|
157
194
|
### v6.4.1 (2026-10-16)
|
|
158
195
|
|
|
159
196
|
- **Changed — new opt-in manager method `generateRelationEvidenceChunks(items, { formatVersion: 'evidence-v1' })`.**
|
package/dist/index.d.ts
CHANGED
|
@@ -4,14 +4,19 @@ import { EVIDENCE_FORMAT, type EvidenceItemInput, type EvidenceStatus, type Evid
|
|
|
4
4
|
import { EmbeddingGate } from './src/embeddingGate.js';
|
|
5
5
|
import type { EmbedPriority } from './src/embeddingGate.js';
|
|
6
6
|
import { BackfillCoordinator } from './src/backfillCoordinator.js';
|
|
7
|
+
import type { RebuildResult, VecTable } from './src/vecSlots.js';
|
|
7
8
|
export declare const LEGACY_QUERY_INSTRUCTION = "Represent this sentence for searching relevant passages: ";
|
|
8
9
|
export declare function embeddingInput(text: string, isQuery: boolean, defaultModel?: boolean): string;
|
|
9
10
|
export declare function makeEmbedFn(model: (input: string, opts: any) => Promise<{
|
|
10
11
|
data: any;
|
|
11
12
|
}>, defaultModel: boolean): (text: string, dims: number, isQuery: boolean) => Promise<Float32Array<ArrayBuffer>>;
|
|
12
13
|
export declare function compileFtsLiteralQuery(q: string): string | null;
|
|
14
|
+
export declare function setPipelineImpl(fn: ((...args: any[]) => Promise<any>) | null): void;
|
|
13
15
|
export declare function resolveDbFilePath(envValue: string | undefined, cwd: string, serverDir: string): string;
|
|
14
16
|
export declare function parseMmapSize(raw: string | undefined): number;
|
|
17
|
+
export declare const MODEL_SESSION_OPTIONS: Readonly<{
|
|
18
|
+
enableCpuMemArena: false;
|
|
19
|
+
}>;
|
|
15
20
|
interface Entity {
|
|
16
21
|
name: string;
|
|
17
22
|
entityType: string;
|
|
@@ -128,6 +133,20 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
128
133
|
__testForceFkOff?: boolean;
|
|
129
134
|
}): Promise<void>;
|
|
130
135
|
private openConnection;
|
|
136
|
+
writeSeq: number;
|
|
137
|
+
private tcBase;
|
|
138
|
+
private lastActivityAt;
|
|
139
|
+
private lastWriteAt;
|
|
140
|
+
private lastSlotCheckAt;
|
|
141
|
+
readonly vectorSlotLast: Partial<Record<VecTable, RebuildResult & {
|
|
142
|
+
at: string;
|
|
143
|
+
}>>;
|
|
144
|
+
private static slotCheckMs;
|
|
145
|
+
private static slotQuietMs;
|
|
146
|
+
maybeRebuildVectors(now?: number): RebuildResult[] | null;
|
|
147
|
+
vectorSlotStats(): Record<string, unknown>;
|
|
148
|
+
private totalChanges;
|
|
149
|
+
noteWrites(): void;
|
|
131
150
|
private ensureCurrentProfile;
|
|
132
151
|
private buildRealLoader;
|
|
133
152
|
startReconciliation(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { migrations } from './src/migrations/migrations.js';
|
|
|
34
34
|
import { EmbeddingGate, GateNotReadyError, GateDisabledError, TerminalConfigError } from './src/embeddingGate.js';
|
|
35
35
|
import { resolveModelCacheDir, preflightCacheDir, artifactKey, ModelDownloadLock, handleLoaderFailure } from './src/modelCache.js';
|
|
36
36
|
import { BackfillCoordinator } from './src/backfillCoordinator.js';
|
|
37
|
+
import { writeVector, deleteVector, registerVecSlotFunctions, InvalidSlotError, slotSummary, rebuildVecTable, rebuildReason, DEFAULT_REBUILD_POLICY } from './src/vecSlots.js';
|
|
37
38
|
import { calendarDate, resolveCalendarTimeZone, stampDatePrefix, stripDatePrefix } from './src/observations/date-prefix.js';
|
|
38
39
|
import os from 'node:os';
|
|
39
40
|
import { createHash } from 'crypto';
|
|
@@ -95,6 +96,12 @@ export function compileFtsLiteralQuery(q) {
|
|
|
95
96
|
return null;
|
|
96
97
|
return terms.map(t => `"${t}"`).join(' OR ');
|
|
97
98
|
}
|
|
99
|
+
// Test-only seam for the real model loader: replaces `pipeline` so a test can see the exact
|
|
100
|
+
// arguments buildRealLoader passes (same rule as setSyncFaultPoint: an argument, never an env var).
|
|
101
|
+
let __pipelineImpl = pipeline;
|
|
102
|
+
export function setPipelineImpl(fn) {
|
|
103
|
+
__pipelineImpl = (fn ?? pipeline);
|
|
104
|
+
}
|
|
98
105
|
// Configure Hugging Face transformers for better compatibility
|
|
99
106
|
if (env.backends?.onnx?.wasm) {
|
|
100
107
|
env.backends.onnx.wasm.wasmPaths = './node_modules/@huggingface/transformers/dist/';
|
|
@@ -142,6 +149,12 @@ const IS_DEFAULT_MODEL_CONFIG = !process.env.EMBEDDING_MODEL || process.env.EMBE
|
|
|
142
149
|
// on 'main' (their vectors are never auto-grandfathered anyway).
|
|
143
150
|
const MODEL_REVISION = IS_DEFAULT_MODEL_CONFIG ? '4de13258303883538bd53b696b452bf8099f0858' : 'main';
|
|
144
151
|
const MODEL_DTYPE = 'fp16';
|
|
152
|
+
// ONNX session options for the embedding model. The CPU memory arena is off: with it on, one long
|
|
153
|
+
// input (12,000 chars = 5,774 tokens) left the process holding its peak (footprint 1.4 → 8.0 GB,
|
|
154
|
+
// measured 2026-10-31 on transformers 3.8.1 and 4.3.2) and every MCP client runs its own engine.
|
|
155
|
+
// Arena off returned to 1.5-2.4 GB with byte-identical vectors; the cost is per-call allocation
|
|
156
|
+
// (query 29 → 33 ms, 484-token chunk 574 → 639 ms on 4.3.2). Not part of the cache artifact key.
|
|
157
|
+
export const MODEL_SESSION_OPTIONS = Object.freeze({ enableCpuMemArena: false });
|
|
145
158
|
// Entity embedding text builder version — mixed into entity input hashes so a
|
|
146
159
|
// builder change re-backfills entities without touching chunk vectors (spec §6c).
|
|
147
160
|
const TEXT_BUILDER_VERSION = 'tb1';
|
|
@@ -314,6 +327,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
314
327
|
openConnection(forceFkOff = false) {
|
|
315
328
|
this.db = new Database(DB_FILE_PATH);
|
|
316
329
|
sqliteVec.load(this.db);
|
|
330
|
+
registerVecSlotFunctions(this.db); // vec_slot_valid() for the D1c searchability predicate
|
|
317
331
|
this.db.pragma('journal_mode = WAL');
|
|
318
332
|
this.db.pragma('synchronous = NORMAL');
|
|
319
333
|
this.db.pragma('busy_timeout = 5000');
|
|
@@ -344,6 +358,119 @@ export class RAGKnowledgeGraphManager {
|
|
|
344
358
|
`for history integrity; refusing to run migrations without it.`);
|
|
345
359
|
}
|
|
346
360
|
}
|
|
361
|
+
// Write-sequence baseline for THIS connection (see noteWrites). A fresh connection starts
|
|
362
|
+
// its own total_changes() count, so the baseline is reset on every open; the announced
|
|
363
|
+
// sequence itself never goes back.
|
|
364
|
+
this.tcBase = this.totalChanges();
|
|
365
|
+
}
|
|
366
|
+
// --- Write-sequence signal (test/write-seq.test.mjs) ------------------------------------
|
|
367
|
+
// One stderr line per observation that THIS connection changed rows:
|
|
368
|
+
// [rag-memory] write-seq <N> (N = 1, 2, 3, … for the life of the process)
|
|
369
|
+
// The framework launcher (mcp-proxy.cjs) records "the DB state this machine left" only
|
|
370
|
+
// after such a line. Before this signal it recorded whenever the file changed and went
|
|
371
|
+
// quiet — which also fired when a cloud sync client swapped the other machine's file in
|
|
372
|
+
// under an idle engine, and that record then refused a correct DB (measured 2026-11-02).
|
|
373
|
+
// total_changes() counts rows changed by this connection (vec0 shadow tables included);
|
|
374
|
+
// reads and other connections do not move it. It is read outside a transaction only, so a
|
|
375
|
+
// statement still inside one is announced after it ends. It also counts rows of a
|
|
376
|
+
// transaction that was rolled back: a line means "this connection's change count went up",
|
|
377
|
+
// not "a commit happened". The launcher consumes a pending sequence once the file it last
|
|
378
|
+
// recorded is still there, the WAL is empty and the file is stable; until then a sequence that
|
|
379
|
+
// came from a rollback can still publish a file another writer or a sync client changed, and
|
|
380
|
+
// that period has no fixed upper bound (hub session 251 review r1 M1 · r3. r2: a WAL-size
|
|
381
|
+
// test was tried and removed — a header-only WAL looks non-empty and another connection's
|
|
382
|
+
// checkpoint hides a real commit).
|
|
383
|
+
// Written with fs.writeSync: the last call runs inside process 'exit', where an async
|
|
384
|
+
// stderr pipe write (macOS) can be lost.
|
|
385
|
+
writeSeq = 0;
|
|
386
|
+
tcBase = 0;
|
|
387
|
+
// ---- vec-slot-reuse D1b: idle verified rebuild of the vec0 tables -------------------------------
|
|
388
|
+
// Initial operating values (2_ s253 r2 Q3: starting values, not optima): look at most every 10 min,
|
|
389
|
+
// only after 60 s without a tool call or an own-connection write, never while a tool call, a backfill
|
|
390
|
+
// scan or a reconciliation is running. A repair request (validity-0 rows seen) skips the 10-min wait.
|
|
391
|
+
// The saving threshold and the live-vector cap live in DEFAULT_REBUILD_POLICY (src/vecSlots.ts).
|
|
392
|
+
lastActivityAt = Date.now();
|
|
393
|
+
lastWriteAt = 0;
|
|
394
|
+
lastSlotCheckAt = 0;
|
|
395
|
+
// Last attempt per table. Process memory only: unknown after a restart (stats says so).
|
|
396
|
+
vectorSlotLast = {};
|
|
397
|
+
static slotCheckMs = Number(process.env.RAG_MEMORY_SLOT_CHECK_MS ?? 600_000);
|
|
398
|
+
static slotQuietMs = Number(process.env.RAG_MEMORY_SLOT_QUIET_MS ?? 60_000);
|
|
399
|
+
maybeRebuildVectors(now = Date.now()) {
|
|
400
|
+
if (!this.db || this.shuttingDown || this.inFlight > 0 || this.db.inTransaction)
|
|
401
|
+
return null;
|
|
402
|
+
if (this.coordinator?.busy)
|
|
403
|
+
return null;
|
|
404
|
+
if (now - Math.max(this.lastActivityAt, this.lastWriteAt) < RAGKnowledgeGraphManager.slotQuietMs)
|
|
405
|
+
return null;
|
|
406
|
+
// A repair request skips the 10-min wait once: only if it arrived after the last check (r3 M5 — otherwise
|
|
407
|
+
// a busy / failed / over-cap attempt would be retried on every 1 s tick).
|
|
408
|
+
const repair = this.coordinator?.repairRequested ?? false;
|
|
409
|
+
const freshRepair = repair && (this.coordinator?.repairRequestedAt ?? 0) > this.lastSlotCheckAt;
|
|
410
|
+
if (!freshRepair && now - this.lastSlotCheckAt < RAGKnowledgeGraphManager.slotCheckMs)
|
|
411
|
+
return null;
|
|
412
|
+
this.lastSlotCheckAt = now;
|
|
413
|
+
const results = [];
|
|
414
|
+
for (const table of ['chunks', 'entity_embeddings']) {
|
|
415
|
+
const r = rebuildVecTable(this.db, table);
|
|
416
|
+
this.vectorSlotLast[table] = { ...r, at: new Date(now).toISOString() };
|
|
417
|
+
results.push(r);
|
|
418
|
+
if (r.status === 'rebuilt') {
|
|
419
|
+
console.error(`🧹 vec0 ${table} rebuilt (${r.reason}): chunks ${r.before?.chunks} -> ${r.after?.chunks}, ` +
|
|
420
|
+
`valid ${r.after?.valid}, dropped validity-0 ${r.before?.invalid}, shared-slot ${r.before?.aliased}, ${r.ms} ms`);
|
|
421
|
+
}
|
|
422
|
+
else if (r.status !== 'not-needed') {
|
|
423
|
+
console.error(`⚠️ vec0 ${table} rebuild ${r.status}${r.error ? `: ${r.error}` : ''}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (repair && results.every(r => r.status === 'rebuilt' || r.status === 'not-needed'))
|
|
427
|
+
this.coordinator?.takeRepairRequest();
|
|
428
|
+
if (results.some(r => r.status === 'rebuilt')) {
|
|
429
|
+
this.coordinator?.invalidateCoverage();
|
|
430
|
+
this.noteWrites();
|
|
431
|
+
this.coordinator?.kick(); // rows dropped as validity-0 are now missing -> re-embedded at their rowid
|
|
432
|
+
}
|
|
433
|
+
return results;
|
|
434
|
+
}
|
|
435
|
+
vectorSlotStats() {
|
|
436
|
+
const out = {};
|
|
437
|
+
for (const table of ['chunks', 'entity_embeddings']) {
|
|
438
|
+
let summary;
|
|
439
|
+
try {
|
|
440
|
+
summary = slotSummary(this.db, table);
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
summary = { error: e.message };
|
|
444
|
+
}
|
|
445
|
+
const pending = 'error' in summary ? 'position-error'
|
|
446
|
+
: (summary.valid > DEFAULT_REBUILD_POLICY.maxLive ? 'over-cap' : (rebuildReason(summary) ?? null));
|
|
447
|
+
const last = this.vectorSlotLast[table];
|
|
448
|
+
out[table] = { ...summary, due: pending,
|
|
449
|
+
last_attempt: last ? { at: last.at, status: last.status, reason: last.reason ?? null, ms: last.ms, error: last.error ?? null }
|
|
450
|
+
: 'unknown (process memory; none since start)' };
|
|
451
|
+
}
|
|
452
|
+
out.policy = { ...DEFAULT_REBUILD_POLICY, check_ms: RAGKnowledgeGraphManager.slotCheckMs, quiet_ms: RAGKnowledgeGraphManager.slotQuietMs,
|
|
453
|
+
repair_requested: this.coordinator?.repairRequested ?? false, vacuum: 'never (maintenance script only)' };
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
totalChanges() {
|
|
457
|
+
if (!this.db)
|
|
458
|
+
return 0;
|
|
459
|
+
return Number(this.db.prepare('select total_changes()').pluck().get());
|
|
460
|
+
}
|
|
461
|
+
noteWrites() {
|
|
462
|
+
try {
|
|
463
|
+
if (!this.db || this.db.inTransaction)
|
|
464
|
+
return;
|
|
465
|
+
const tc = this.totalChanges();
|
|
466
|
+
if (tc <= this.tcBase)
|
|
467
|
+
return;
|
|
468
|
+
this.tcBase = tc;
|
|
469
|
+
this.lastWriteAt = Date.now();
|
|
470
|
+
this.writeSeq += 1;
|
|
471
|
+
fsSync.writeSync(2, `[rag-memory] write-seq ${this.writeSeq}\n`);
|
|
472
|
+
}
|
|
473
|
+
catch { /* a dead handle is the reopen path's business; the next tick tries again */ }
|
|
347
474
|
}
|
|
348
475
|
// Upsert the stored-vector compatibility profile (spec §6c layer 2) and record
|
|
349
476
|
// retrieval config in server_meta (layer 3 — never a backfill trigger).
|
|
@@ -387,7 +514,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
387
514
|
env.allowRemoteModels = true;
|
|
388
515
|
env.allowLocalModels = true;
|
|
389
516
|
console.error(`🤖 Loading embedding model: ${EMBEDDING_MODEL} (1024-dim, cache=${cacheDir})...`);
|
|
390
|
-
const model = await
|
|
517
|
+
const model = await __pipelineImpl('feature-extraction', EMBEDDING_MODEL, { revision: MODEL_REVISION, dtype: MODEL_DTYPE, cache_dir: cacheDir,
|
|
518
|
+
session_options: { ...MODEL_SESSION_OPTIONS } });
|
|
391
519
|
// dims fail-fast (spec §2 / beta B8): probe the ACTUAL output length — a
|
|
392
520
|
// 384/768-dim custom model must fail here with a clear message, not at
|
|
393
521
|
// every subsequent vector write.
|
|
@@ -539,7 +667,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
539
667
|
const meta = this.db.prepare(`SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?`)
|
|
540
668
|
.get(entityId);
|
|
541
669
|
if (meta) {
|
|
542
|
-
this.db
|
|
670
|
+
deleteVector(this.db, 'entity_embeddings', Number(meta.rowid));
|
|
543
671
|
this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`).run(entityId);
|
|
544
672
|
}
|
|
545
673
|
deleteStaleKgChunks(this.db, entityId);
|
|
@@ -554,7 +682,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
554
682
|
if (!meta)
|
|
555
683
|
return;
|
|
556
684
|
const tx = this.db.transaction(() => {
|
|
557
|
-
this.db
|
|
685
|
+
deleteVector(this.db, 'entity_embeddings', Number(meta.rowid));
|
|
558
686
|
this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`).run(entityId);
|
|
559
687
|
});
|
|
560
688
|
tx();
|
|
@@ -584,8 +712,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
584
712
|
return false; // inputs changed
|
|
585
713
|
if (this.currentProfileId !== captured.profile_id)
|
|
586
714
|
return false; // profile changed during inference
|
|
587
|
-
this.db.
|
|
588
|
-
this.db.prepare(`INSERT INTO chunks (rowid, embedding) VALUES (${rowid}, ?)`).run(Buffer.from(embedding.buffer));
|
|
715
|
+
writeVector(this.db, 'chunks', rowid, Buffer.from(embedding.buffer)); // same slot when valid (vec-slot-reuse)
|
|
589
716
|
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
590
717
|
.run(captured.text_sha256, captured.profile_id, rowid);
|
|
591
718
|
const meta = parseEvidenceMetadata(cur.metadata);
|
|
@@ -597,7 +724,17 @@ export class RAGKnowledgeGraphManager {
|
|
|
597
724
|
this.db.prepare(`DELETE FROM embedding_backfill_failures WHERE kind = 'chunk' AND target_id = ?`).run(String(rowid));
|
|
598
725
|
return true;
|
|
599
726
|
});
|
|
600
|
-
|
|
727
|
+
let written;
|
|
728
|
+
try {
|
|
729
|
+
written = tx();
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
if (e instanceof InvalidSlotError) {
|
|
733
|
+
this.coordinator?.requestRepair();
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
throw e;
|
|
737
|
+
}
|
|
601
738
|
this.coordinator?.invalidateCoverage();
|
|
602
739
|
return written;
|
|
603
740
|
}
|
|
@@ -635,14 +772,23 @@ export class RAGKnowledgeGraphManager {
|
|
|
635
772
|
const cur = this.db.prepare(`SELECT text FROM chunk_metadata WHERE rowid = ?`).get(rowid);
|
|
636
773
|
if (!cur || cur.text !== row.text)
|
|
637
774
|
return false; // superseded — discard
|
|
638
|
-
this.db
|
|
639
|
-
this.db.prepare(`INSERT INTO chunks (rowid, embedding) VALUES (${safe}, ?)`).run(Buffer.from(embedding.buffer));
|
|
775
|
+
writeVector(this.db, 'chunks', safe, Buffer.from(embedding.buffer));
|
|
640
776
|
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
641
777
|
.run(hash, this.currentProfileId, rowid);
|
|
642
778
|
this.db.prepare(`DELETE FROM embedding_backfill_failures WHERE kind = 'chunk' AND target_id = ?`).run(String(rowid));
|
|
643
779
|
return true;
|
|
644
780
|
});
|
|
645
|
-
|
|
781
|
+
let written;
|
|
782
|
+
try {
|
|
783
|
+
written = tx();
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
if (e instanceof InvalidSlotError) {
|
|
787
|
+
this.coordinator?.requestRepair();
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
throw e;
|
|
791
|
+
}
|
|
646
792
|
this.coordinator?.invalidateCoverage();
|
|
647
793
|
return written;
|
|
648
794
|
}
|
|
@@ -859,6 +1005,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
859
1005
|
console.error(`⚠️ reopen storm (${REOPEN_STORM_MAX} in ${REOPEN_STORM_WINDOW_MS / 1000}s) — watch paused, reason=${reason}`);
|
|
860
1006
|
return false;
|
|
861
1007
|
}
|
|
1008
|
+
this.noteWrites(); // rows changed on the old connection are announced before its count is gone
|
|
862
1009
|
const old = this.db;
|
|
863
1010
|
// Salvage: frames committed to the old WAL after the last checkpoint (≤1 s) go to the main
|
|
864
1011
|
// file if the old descriptor is still readable. An I/O error here just means they are gone.
|
|
@@ -964,14 +1111,22 @@ export class RAGKnowledgeGraphManager {
|
|
|
964
1111
|
catch (e) {
|
|
965
1112
|
console.error(`⚠️ wal tick: ${e.message}`);
|
|
966
1113
|
}
|
|
1114
|
+
try {
|
|
1115
|
+
this.maybeRebuildVectors();
|
|
1116
|
+
}
|
|
1117
|
+
catch (e) {
|
|
1118
|
+
console.error(`⚠️ vec0 rebuild check: ${e.message}`);
|
|
1119
|
+
}
|
|
967
1120
|
this.checkpointWal();
|
|
1121
|
+
this.noteWrites();
|
|
968
1122
|
}, tickMs);
|
|
969
1123
|
this.walTimer.unref();
|
|
970
1124
|
}
|
|
971
1125
|
noteActivity(delayMs = 200) {
|
|
1126
|
+
this.lastActivityAt = Date.now();
|
|
972
1127
|
if (this.walSoon)
|
|
973
1128
|
return;
|
|
974
|
-
this.walSoon = setTimeout(() => { this.walSoon = null; this.checkpointWal(); }, delayMs);
|
|
1129
|
+
this.walSoon = setTimeout(() => { this.walSoon = null; this.checkpointWal(); this.noteWrites(); }, delayMs);
|
|
975
1130
|
this.walSoon.unref();
|
|
976
1131
|
}
|
|
977
1132
|
cleanup() {
|
|
@@ -993,6 +1148,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
993
1148
|
// close() folds the WAL only for the last connection; with other engines attached it
|
|
994
1149
|
// would leave every frame behind.
|
|
995
1150
|
this.checkpointWal();
|
|
1151
|
+
this.noteWrites();
|
|
996
1152
|
this.db.close();
|
|
997
1153
|
this.db = null;
|
|
998
1154
|
}
|
|
@@ -1306,13 +1462,11 @@ export class RAGKnowledgeGraphManager {
|
|
|
1306
1462
|
SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
|
|
1307
1463
|
`).get(eid);
|
|
1308
1464
|
if (embeddingMetadata) {
|
|
1309
|
-
const
|
|
1310
|
-
DELETE FROM entity_embeddings WHERE rowid = ?
|
|
1311
|
-
`).run(embeddingMetadata.rowid);
|
|
1465
|
+
const vecState = deleteVector(this.db, 'entity_embeddings', Number(embeddingMetadata.rowid));
|
|
1312
1466
|
const metadata = this.db.prepare(`
|
|
1313
1467
|
DELETE FROM entity_embedding_metadata WHERE entity_id = ?
|
|
1314
1468
|
`).run(eid);
|
|
1315
|
-
if (
|
|
1469
|
+
if (vecState === 'valid' || metadata.changes > 0) {
|
|
1316
1470
|
console.error(` ├─ Removed entity embeddings for '${name}'`);
|
|
1317
1471
|
}
|
|
1318
1472
|
}
|
|
@@ -1433,7 +1587,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1433
1587
|
const meta = this.db.prepare(`SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?`)
|
|
1434
1588
|
.get(p.entityId);
|
|
1435
1589
|
if (meta) {
|
|
1436
|
-
this.db
|
|
1590
|
+
deleteVector(this.db, 'entity_embeddings', Number(meta.rowid));
|
|
1437
1591
|
this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`)
|
|
1438
1592
|
.run(p.entityId);
|
|
1439
1593
|
}
|
|
@@ -2046,19 +2200,40 @@ export class RAGKnowledgeGraphManager {
|
|
|
2046
2200
|
SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
|
|
2047
2201
|
`).get(entityId);
|
|
2048
2202
|
if (existingMetadata) {
|
|
2049
|
-
|
|
2050
|
-
|
|
2203
|
+
// vec-slot-reuse: keep the rowid and rewrite the vector in place (DELETE + INSERT with a new
|
|
2204
|
+
// rowid leaked one vec0 slot per re-embed). The metadata row is updated, not replaced.
|
|
2205
|
+
writeVector(this.db, 'entity_embeddings', Number(existingMetadata.rowid), Buffer.from(embedding.buffer));
|
|
2206
|
+
this.db.prepare(`
|
|
2207
|
+
UPDATE entity_embedding_metadata
|
|
2208
|
+
SET embedding_text = ?, input_hash = ?, profile_id = ?, provenance_state = 'verified'
|
|
2209
|
+
WHERE rowid = ?
|
|
2210
|
+
`).run(embeddingText, inputHash, this.currentProfileId, existingMetadata.rowid);
|
|
2211
|
+
return true;
|
|
2051
2212
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2213
|
+
// r4 M2: metadata rows can now outlive their vector (kept unstamped to keep the rowid), so the vec0
|
|
2214
|
+
// auto rowid could collide with one. Allocate past both tables, in this transaction.
|
|
2215
|
+
const nextRowid = BigInt(this.db.prepare(`SELECT MAX(r) m FROM (
|
|
2216
|
+
SELECT MAX(rowid) r FROM entity_embedding_metadata UNION ALL SELECT MAX(rowid) r FROM entity_embeddings_rowids)`)
|
|
2217
|
+
.safeIntegers(true).get().m ?? 0n) + 1n;
|
|
2218
|
+
writeVector(this.db, 'entity_embeddings', nextRowid, Buffer.from(embedding.buffer));
|
|
2219
|
+
const result = { lastInsertRowid: nextRowid };
|
|
2055
2220
|
this.db.prepare(`
|
|
2056
2221
|
INSERT INTO entity_embedding_metadata (rowid, entity_id, embedding_text, input_hash, profile_id, provenance_state)
|
|
2057
2222
|
VALUES (?, ?, ?, ?, ?, 'verified')
|
|
2058
2223
|
`).run(result.lastInsertRowid, entityId, embeddingText, inputHash, this.currentProfileId);
|
|
2059
2224
|
return true;
|
|
2060
2225
|
});
|
|
2061
|
-
|
|
2226
|
+
let written;
|
|
2227
|
+
try {
|
|
2228
|
+
written = writeTx();
|
|
2229
|
+
}
|
|
2230
|
+
catch (e) {
|
|
2231
|
+
if (e instanceof InvalidSlotError) {
|
|
2232
|
+
this.coordinator?.requestRepair();
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
throw e;
|
|
2236
|
+
}
|
|
2062
2237
|
if (!written) {
|
|
2063
2238
|
console.error(`⏭️ discarded superseded embedding for ${entityId} (entity changed during inference)`);
|
|
2064
2239
|
}
|
|
@@ -2280,7 +2455,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2280
2455
|
let deletedAssociations = 0;
|
|
2281
2456
|
for (const chunk of existingChunks) {
|
|
2282
2457
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
2283
|
-
this.db
|
|
2458
|
+
deleteVector(this.db, 'chunks', safeRowid(chunk.rowid));
|
|
2284
2459
|
deletedVectors++;
|
|
2285
2460
|
const associations = this.db.prepare(`
|
|
2286
2461
|
DELETE FROM chunk_entities WHERE chunk_rowid = ?
|
|
@@ -2625,7 +2800,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2625
2800
|
const existing = db.prepare(`SELECT rowid FROM chunk_metadata WHERE document_id = ?`).all(documentId);
|
|
2626
2801
|
for (const ch of existing) {
|
|
2627
2802
|
db.prepare(`DELETE FROM chunk_entities WHERE chunk_rowid = ?`).run(ch.rowid);
|
|
2628
|
-
db
|
|
2803
|
+
deleteVector(db, 'chunks', safeRowid(ch.rowid));
|
|
2629
2804
|
}
|
|
2630
2805
|
db.prepare(`DELETE FROM chunk_metadata WHERE document_id = ?`).run(documentId);
|
|
2631
2806
|
deleteKgChunkRows(db, citedEvidence);
|
|
@@ -2640,9 +2815,23 @@ export class RAGKnowledgeGraphManager {
|
|
|
2640
2815
|
`).run(chunkId, documentId, seg.chunk_index, seg.text, seg.start_pos, seg.end_pos, seg.start_token, seg.end_token);
|
|
2641
2816
|
const rowid = Number(info.lastInsertRowid);
|
|
2642
2817
|
if (vec) {
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2818
|
+
// A validity-0 row left at a reused rowid cannot be written over (vec-slot-reuse): the chunk then stays
|
|
2819
|
+
// vectorless (missing) until the idle rebuild drops that row and the backfill embeds it.
|
|
2820
|
+
let wrote = true;
|
|
2821
|
+
try {
|
|
2822
|
+
writeVector(db, 'chunks', rowid, vec);
|
|
2823
|
+
}
|
|
2824
|
+
catch (e) {
|
|
2825
|
+
if (e instanceof InvalidSlotError) {
|
|
2826
|
+
wrote = false;
|
|
2827
|
+
this.coordinator?.requestRepair();
|
|
2828
|
+
}
|
|
2829
|
+
else
|
|
2830
|
+
throw e;
|
|
2831
|
+
}
|
|
2832
|
+
if (wrote)
|
|
2833
|
+
db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = ? WHERE rowid = ?`)
|
|
2834
|
+
.run(shaHex(seg.text), this.currentProfileId, provenance, rowid);
|
|
2646
2835
|
}
|
|
2647
2836
|
}
|
|
2648
2837
|
if (oldRowids.length > 0) {
|
|
@@ -2789,10 +2978,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2789
2978
|
// Store in vector table (+ verified provenance, §6a-2 atomic)
|
|
2790
2979
|
try {
|
|
2791
2980
|
const tx = this.db.transaction(() => {
|
|
2792
|
-
this.db
|
|
2793
|
-
this.db.prepare(`
|
|
2794
|
-
INSERT INTO chunks (rowid, embedding) VALUES (${rowid}, ?)
|
|
2795
|
-
`).run(Buffer.from(embedding.buffer));
|
|
2981
|
+
writeVector(this.db, 'chunks', safeRowid(rowid), Buffer.from(embedding.buffer));
|
|
2796
2982
|
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
2797
2983
|
.run(createHash('sha256').update(chunk.text).digest('hex'), this.currentProfileId, rowid);
|
|
2798
2984
|
});
|
|
@@ -2800,6 +2986,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
2800
2986
|
embeddedCount++;
|
|
2801
2987
|
}
|
|
2802
2988
|
catch (error) {
|
|
2989
|
+
if (error instanceof InvalidSlotError)
|
|
2990
|
+
this.coordinator?.requestRepair();
|
|
2803
2991
|
const errMsg = `chunk ${chunk.chunk_id} (rowid=${rowid}, type=${typeof chunk.rowid}): ${error instanceof Error ? error.message : String(error)}`;
|
|
2804
2992
|
console.error(`Failed to embed ${errMsg}`);
|
|
2805
2993
|
errors.push(errMsg);
|
|
@@ -3238,7 +3426,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
3238
3426
|
`).run(chunk.rowid);
|
|
3239
3427
|
deletedAssociations += associations.changes;
|
|
3240
3428
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
3241
|
-
db
|
|
3429
|
+
deleteVector(db, 'chunks', safeRowid(chunk.rowid));
|
|
3242
3430
|
deletedVectors++;
|
|
3243
3431
|
}
|
|
3244
3432
|
const metadata = db.prepare(`
|
|
@@ -3486,10 +3674,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
3486
3674
|
const orphanChunks = this.db.prepare(`SELECT rowid FROM chunk_metadata WHERE chunk_type IN ('entity','relationship')`)
|
|
3487
3675
|
.all();
|
|
3488
3676
|
for (const c of orphanChunks) {
|
|
3489
|
-
this.db
|
|
3677
|
+
deleteVector(this.db, 'chunks', Number(c.rowid));
|
|
3490
3678
|
this.db.prepare(`DELETE FROM chunk_metadata WHERE rowid = ?`).run(c.rowid);
|
|
3491
3679
|
}
|
|
3492
|
-
this.db.
|
|
3680
|
+
for (const m of this.db.prepare(`SELECT rowid FROM entity_embedding_metadata`).all()) {
|
|
3681
|
+
deleteVector(this.db, 'entity_embeddings', Number(m.rowid)); // validity-0 rows are left to the rebuild
|
|
3682
|
+
}
|
|
3493
3683
|
this.db.exec(`DELETE FROM entity_embedding_metadata`);
|
|
3494
3684
|
console.error('🗑️ Cleared existing data for full import');
|
|
3495
3685
|
}
|
|
@@ -4374,9 +4564,17 @@ export class RAGKnowledgeGraphManager {
|
|
|
4374
4564
|
reconciliation_state: this.coordinator?.reconState ?? 'n/a',
|
|
4375
4565
|
reconciliation_last_error: this.coordinator?.reconLastError ?? null,
|
|
4376
4566
|
coverage: cov ? {
|
|
4377
|
-
|
|
4378
|
-
|
|
4567
|
+
// vec-slot-reuse D1c: an unresolvable slot map is reported as error with missing = null, not as a count.
|
|
4568
|
+
chunk: cov.chunk.error
|
|
4569
|
+
? { total: cov.chunk.total, embedded: null, verified: null, legacy_assumed: null, missing: null, error: cov.chunk.error }
|
|
4570
|
+
: { total: cov.chunk.total, embedded: cov.chunk.embedded, verified: cov.chunk.verified, legacy_assumed: cov.chunk.legacy_assumed, missing: cov.chunk.total - cov.chunk.embedded },
|
|
4571
|
+
entity: cov.entity.error
|
|
4572
|
+
? { total: cov.entity.total, embedded: null, verified: null, legacy_assumed: null, missing: null, error: cov.entity.error }
|
|
4573
|
+
: { total: cov.entity.total, embedded: cov.entity.embedded, verified: cov.entity.verified, legacy_assumed: cov.entity.legacy_assumed, missing: cov.entity.total - cov.entity.embedded },
|
|
4379
4574
|
} : null,
|
|
4575
|
+
// vec-slot-reuse: per vec0 table = slots, searchable, validity-0 rows, reclaimable chunks, why a rebuild
|
|
4576
|
+
// is (not) due, and the last attempt of this process. Read-only: computing it writes nothing.
|
|
4577
|
+
vector_slots: this.db ? this.vectorSlotStats() : null,
|
|
4380
4578
|
}
|
|
4381
4579
|
};
|
|
4382
4580
|
}
|
|
@@ -4888,6 +5086,7 @@ async function main() {
|
|
|
4888
5086
|
// Synchronously, before anything is awaited: a host may SIGKILL before the settle
|
|
4889
5087
|
// sequence below completes (codex: ~185 ms after SIGTERM).
|
|
4890
5088
|
ragKgManager.checkpointWal();
|
|
5089
|
+
ragKgManager.noteWrites();
|
|
4891
5090
|
void (async () => {
|
|
4892
5091
|
try {
|
|
4893
5092
|
await server.close();
|
|
@@ -6,12 +6,14 @@ export interface CoverageSnapshot {
|
|
|
6
6
|
embedded: number;
|
|
7
7
|
verified: number;
|
|
8
8
|
legacy_assumed: number;
|
|
9
|
+
error?: string;
|
|
9
10
|
};
|
|
10
11
|
entity: {
|
|
11
12
|
total: number;
|
|
12
13
|
embedded: number;
|
|
13
14
|
verified: number;
|
|
14
15
|
legacy_assumed: number;
|
|
16
|
+
error?: string;
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
export interface CoordinatorDeps {
|
|
@@ -37,17 +39,25 @@ export declare class BackfillCoordinator {
|
|
|
37
39
|
private shuttingDown;
|
|
38
40
|
private connLossReruns;
|
|
39
41
|
private scanning;
|
|
42
|
+
private repairWanted;
|
|
43
|
+
readonly slotErrors: Map<string, string>;
|
|
44
|
+
repairRequestedAt: number;
|
|
45
|
+
requestRepair(): void;
|
|
46
|
+
takeRepairRequest(): boolean;
|
|
47
|
+
get repairRequested(): boolean;
|
|
40
48
|
private scanPromise;
|
|
41
49
|
private rerunRequested;
|
|
42
50
|
private snapshot;
|
|
43
51
|
constructor(deps: CoordinatorDeps);
|
|
44
52
|
get reconState(): ReconState;
|
|
53
|
+
get busy(): boolean;
|
|
45
54
|
get reconLastError(): string | undefined;
|
|
46
55
|
get eligible(): boolean;
|
|
47
56
|
private countNullWithVector;
|
|
48
57
|
private countRepairables;
|
|
49
58
|
private sanitize;
|
|
50
59
|
runReconciliation(): Promise<void>;
|
|
60
|
+
private reconActive;
|
|
51
61
|
private reconcileOnce;
|
|
52
62
|
private reconcileEntities;
|
|
53
63
|
private reconcileChunks;
|