rag-memory-epf-mcp 6.4.2 → 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 +24 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +220 -34
- 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 +10 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,6 +154,30 @@ storeDocument(id, content, metadata)
|
|
|
154
154
|
|
|
155
155
|
## Changelog
|
|
156
156
|
|
|
157
|
+
### v6.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
|
+
|
|
157
181
|
### v6.4.2 (2026-10-31)
|
|
158
182
|
|
|
159
183
|
- **Fixed — the engine no longer keeps the peak memory of its largest embedding.** The ONNX session is opened with
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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<{
|
|
@@ -132,6 +133,20 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
132
133
|
__testForceFkOff?: boolean;
|
|
133
134
|
}): Promise<void>;
|
|
134
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;
|
|
135
150
|
private ensureCurrentProfile;
|
|
136
151
|
private buildRealLoader;
|
|
137
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';
|
|
@@ -326,6 +327,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
326
327
|
openConnection(forceFkOff = false) {
|
|
327
328
|
this.db = new Database(DB_FILE_PATH);
|
|
328
329
|
sqliteVec.load(this.db);
|
|
330
|
+
registerVecSlotFunctions(this.db); // vec_slot_valid() for the D1c searchability predicate
|
|
329
331
|
this.db.pragma('journal_mode = WAL');
|
|
330
332
|
this.db.pragma('synchronous = NORMAL');
|
|
331
333
|
this.db.pragma('busy_timeout = 5000');
|
|
@@ -356,6 +358,119 @@ export class RAGKnowledgeGraphManager {
|
|
|
356
358
|
`for history integrity; refusing to run migrations without it.`);
|
|
357
359
|
}
|
|
358
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 */ }
|
|
359
474
|
}
|
|
360
475
|
// Upsert the stored-vector compatibility profile (spec §6c layer 2) and record
|
|
361
476
|
// retrieval config in server_meta (layer 3 — never a backfill trigger).
|
|
@@ -552,7 +667,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
552
667
|
const meta = this.db.prepare(`SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?`)
|
|
553
668
|
.get(entityId);
|
|
554
669
|
if (meta) {
|
|
555
|
-
this.db
|
|
670
|
+
deleteVector(this.db, 'entity_embeddings', Number(meta.rowid));
|
|
556
671
|
this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`).run(entityId);
|
|
557
672
|
}
|
|
558
673
|
deleteStaleKgChunks(this.db, entityId);
|
|
@@ -567,7 +682,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
567
682
|
if (!meta)
|
|
568
683
|
return;
|
|
569
684
|
const tx = this.db.transaction(() => {
|
|
570
|
-
this.db
|
|
685
|
+
deleteVector(this.db, 'entity_embeddings', Number(meta.rowid));
|
|
571
686
|
this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`).run(entityId);
|
|
572
687
|
});
|
|
573
688
|
tx();
|
|
@@ -597,8 +712,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
597
712
|
return false; // inputs changed
|
|
598
713
|
if (this.currentProfileId !== captured.profile_id)
|
|
599
714
|
return false; // profile changed during inference
|
|
600
|
-
this.db.
|
|
601
|
-
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)
|
|
602
716
|
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
603
717
|
.run(captured.text_sha256, captured.profile_id, rowid);
|
|
604
718
|
const meta = parseEvidenceMetadata(cur.metadata);
|
|
@@ -610,7 +724,17 @@ export class RAGKnowledgeGraphManager {
|
|
|
610
724
|
this.db.prepare(`DELETE FROM embedding_backfill_failures WHERE kind = 'chunk' AND target_id = ?`).run(String(rowid));
|
|
611
725
|
return true;
|
|
612
726
|
});
|
|
613
|
-
|
|
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
|
+
}
|
|
614
738
|
this.coordinator?.invalidateCoverage();
|
|
615
739
|
return written;
|
|
616
740
|
}
|
|
@@ -648,14 +772,23 @@ export class RAGKnowledgeGraphManager {
|
|
|
648
772
|
const cur = this.db.prepare(`SELECT text FROM chunk_metadata WHERE rowid = ?`).get(rowid);
|
|
649
773
|
if (!cur || cur.text !== row.text)
|
|
650
774
|
return false; // superseded — discard
|
|
651
|
-
this.db
|
|
652
|
-
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));
|
|
653
776
|
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
654
777
|
.run(hash, this.currentProfileId, rowid);
|
|
655
778
|
this.db.prepare(`DELETE FROM embedding_backfill_failures WHERE kind = 'chunk' AND target_id = ?`).run(String(rowid));
|
|
656
779
|
return true;
|
|
657
780
|
});
|
|
658
|
-
|
|
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
|
+
}
|
|
659
792
|
this.coordinator?.invalidateCoverage();
|
|
660
793
|
return written;
|
|
661
794
|
}
|
|
@@ -872,6 +1005,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
872
1005
|
console.error(`⚠️ reopen storm (${REOPEN_STORM_MAX} in ${REOPEN_STORM_WINDOW_MS / 1000}s) — watch paused, reason=${reason}`);
|
|
873
1006
|
return false;
|
|
874
1007
|
}
|
|
1008
|
+
this.noteWrites(); // rows changed on the old connection are announced before its count is gone
|
|
875
1009
|
const old = this.db;
|
|
876
1010
|
// Salvage: frames committed to the old WAL after the last checkpoint (≤1 s) go to the main
|
|
877
1011
|
// file if the old descriptor is still readable. An I/O error here just means they are gone.
|
|
@@ -977,14 +1111,22 @@ export class RAGKnowledgeGraphManager {
|
|
|
977
1111
|
catch (e) {
|
|
978
1112
|
console.error(`⚠️ wal tick: ${e.message}`);
|
|
979
1113
|
}
|
|
1114
|
+
try {
|
|
1115
|
+
this.maybeRebuildVectors();
|
|
1116
|
+
}
|
|
1117
|
+
catch (e) {
|
|
1118
|
+
console.error(`⚠️ vec0 rebuild check: ${e.message}`);
|
|
1119
|
+
}
|
|
980
1120
|
this.checkpointWal();
|
|
1121
|
+
this.noteWrites();
|
|
981
1122
|
}, tickMs);
|
|
982
1123
|
this.walTimer.unref();
|
|
983
1124
|
}
|
|
984
1125
|
noteActivity(delayMs = 200) {
|
|
1126
|
+
this.lastActivityAt = Date.now();
|
|
985
1127
|
if (this.walSoon)
|
|
986
1128
|
return;
|
|
987
|
-
this.walSoon = setTimeout(() => { this.walSoon = null; this.checkpointWal(); }, delayMs);
|
|
1129
|
+
this.walSoon = setTimeout(() => { this.walSoon = null; this.checkpointWal(); this.noteWrites(); }, delayMs);
|
|
988
1130
|
this.walSoon.unref();
|
|
989
1131
|
}
|
|
990
1132
|
cleanup() {
|
|
@@ -1006,6 +1148,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1006
1148
|
// close() folds the WAL only for the last connection; with other engines attached it
|
|
1007
1149
|
// would leave every frame behind.
|
|
1008
1150
|
this.checkpointWal();
|
|
1151
|
+
this.noteWrites();
|
|
1009
1152
|
this.db.close();
|
|
1010
1153
|
this.db = null;
|
|
1011
1154
|
}
|
|
@@ -1319,13 +1462,11 @@ export class RAGKnowledgeGraphManager {
|
|
|
1319
1462
|
SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
|
|
1320
1463
|
`).get(eid);
|
|
1321
1464
|
if (embeddingMetadata) {
|
|
1322
|
-
const
|
|
1323
|
-
DELETE FROM entity_embeddings WHERE rowid = ?
|
|
1324
|
-
`).run(embeddingMetadata.rowid);
|
|
1465
|
+
const vecState = deleteVector(this.db, 'entity_embeddings', Number(embeddingMetadata.rowid));
|
|
1325
1466
|
const metadata = this.db.prepare(`
|
|
1326
1467
|
DELETE FROM entity_embedding_metadata WHERE entity_id = ?
|
|
1327
1468
|
`).run(eid);
|
|
1328
|
-
if (
|
|
1469
|
+
if (vecState === 'valid' || metadata.changes > 0) {
|
|
1329
1470
|
console.error(` ├─ Removed entity embeddings for '${name}'`);
|
|
1330
1471
|
}
|
|
1331
1472
|
}
|
|
@@ -1446,7 +1587,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1446
1587
|
const meta = this.db.prepare(`SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?`)
|
|
1447
1588
|
.get(p.entityId);
|
|
1448
1589
|
if (meta) {
|
|
1449
|
-
this.db
|
|
1590
|
+
deleteVector(this.db, 'entity_embeddings', Number(meta.rowid));
|
|
1450
1591
|
this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`)
|
|
1451
1592
|
.run(p.entityId);
|
|
1452
1593
|
}
|
|
@@ -2059,19 +2200,40 @@ export class RAGKnowledgeGraphManager {
|
|
|
2059
2200
|
SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
|
|
2060
2201
|
`).get(entityId);
|
|
2061
2202
|
if (existingMetadata) {
|
|
2062
|
-
|
|
2063
|
-
|
|
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;
|
|
2064
2212
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
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 };
|
|
2068
2220
|
this.db.prepare(`
|
|
2069
2221
|
INSERT INTO entity_embedding_metadata (rowid, entity_id, embedding_text, input_hash, profile_id, provenance_state)
|
|
2070
2222
|
VALUES (?, ?, ?, ?, ?, 'verified')
|
|
2071
2223
|
`).run(result.lastInsertRowid, entityId, embeddingText, inputHash, this.currentProfileId);
|
|
2072
2224
|
return true;
|
|
2073
2225
|
});
|
|
2074
|
-
|
|
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
|
+
}
|
|
2075
2237
|
if (!written) {
|
|
2076
2238
|
console.error(`⏭️ discarded superseded embedding for ${entityId} (entity changed during inference)`);
|
|
2077
2239
|
}
|
|
@@ -2293,7 +2455,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2293
2455
|
let deletedAssociations = 0;
|
|
2294
2456
|
for (const chunk of existingChunks) {
|
|
2295
2457
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
2296
|
-
this.db
|
|
2458
|
+
deleteVector(this.db, 'chunks', safeRowid(chunk.rowid));
|
|
2297
2459
|
deletedVectors++;
|
|
2298
2460
|
const associations = this.db.prepare(`
|
|
2299
2461
|
DELETE FROM chunk_entities WHERE chunk_rowid = ?
|
|
@@ -2638,7 +2800,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2638
2800
|
const existing = db.prepare(`SELECT rowid FROM chunk_metadata WHERE document_id = ?`).all(documentId);
|
|
2639
2801
|
for (const ch of existing) {
|
|
2640
2802
|
db.prepare(`DELETE FROM chunk_entities WHERE chunk_rowid = ?`).run(ch.rowid);
|
|
2641
|
-
db
|
|
2803
|
+
deleteVector(db, 'chunks', safeRowid(ch.rowid));
|
|
2642
2804
|
}
|
|
2643
2805
|
db.prepare(`DELETE FROM chunk_metadata WHERE document_id = ?`).run(documentId);
|
|
2644
2806
|
deleteKgChunkRows(db, citedEvidence);
|
|
@@ -2653,9 +2815,23 @@ export class RAGKnowledgeGraphManager {
|
|
|
2653
2815
|
`).run(chunkId, documentId, seg.chunk_index, seg.text, seg.start_pos, seg.end_pos, seg.start_token, seg.end_token);
|
|
2654
2816
|
const rowid = Number(info.lastInsertRowid);
|
|
2655
2817
|
if (vec) {
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
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);
|
|
2659
2835
|
}
|
|
2660
2836
|
}
|
|
2661
2837
|
if (oldRowids.length > 0) {
|
|
@@ -2802,10 +2978,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2802
2978
|
// Store in vector table (+ verified provenance, §6a-2 atomic)
|
|
2803
2979
|
try {
|
|
2804
2980
|
const tx = this.db.transaction(() => {
|
|
2805
|
-
this.db
|
|
2806
|
-
this.db.prepare(`
|
|
2807
|
-
INSERT INTO chunks (rowid, embedding) VALUES (${rowid}, ?)
|
|
2808
|
-
`).run(Buffer.from(embedding.buffer));
|
|
2981
|
+
writeVector(this.db, 'chunks', safeRowid(rowid), Buffer.from(embedding.buffer));
|
|
2809
2982
|
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
2810
2983
|
.run(createHash('sha256').update(chunk.text).digest('hex'), this.currentProfileId, rowid);
|
|
2811
2984
|
});
|
|
@@ -2813,6 +2986,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
2813
2986
|
embeddedCount++;
|
|
2814
2987
|
}
|
|
2815
2988
|
catch (error) {
|
|
2989
|
+
if (error instanceof InvalidSlotError)
|
|
2990
|
+
this.coordinator?.requestRepair();
|
|
2816
2991
|
const errMsg = `chunk ${chunk.chunk_id} (rowid=${rowid}, type=${typeof chunk.rowid}): ${error instanceof Error ? error.message : String(error)}`;
|
|
2817
2992
|
console.error(`Failed to embed ${errMsg}`);
|
|
2818
2993
|
errors.push(errMsg);
|
|
@@ -3251,7 +3426,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
3251
3426
|
`).run(chunk.rowid);
|
|
3252
3427
|
deletedAssociations += associations.changes;
|
|
3253
3428
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
3254
|
-
db
|
|
3429
|
+
deleteVector(db, 'chunks', safeRowid(chunk.rowid));
|
|
3255
3430
|
deletedVectors++;
|
|
3256
3431
|
}
|
|
3257
3432
|
const metadata = db.prepare(`
|
|
@@ -3499,10 +3674,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
3499
3674
|
const orphanChunks = this.db.prepare(`SELECT rowid FROM chunk_metadata WHERE chunk_type IN ('entity','relationship')`)
|
|
3500
3675
|
.all();
|
|
3501
3676
|
for (const c of orphanChunks) {
|
|
3502
|
-
this.db
|
|
3677
|
+
deleteVector(this.db, 'chunks', Number(c.rowid));
|
|
3503
3678
|
this.db.prepare(`DELETE FROM chunk_metadata WHERE rowid = ?`).run(c.rowid);
|
|
3504
3679
|
}
|
|
3505
|
-
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
|
+
}
|
|
3506
3683
|
this.db.exec(`DELETE FROM entity_embedding_metadata`);
|
|
3507
3684
|
console.error('🗑️ Cleared existing data for full import');
|
|
3508
3685
|
}
|
|
@@ -4387,9 +4564,17 @@ export class RAGKnowledgeGraphManager {
|
|
|
4387
4564
|
reconciliation_state: this.coordinator?.reconState ?? 'n/a',
|
|
4388
4565
|
reconciliation_last_error: this.coordinator?.reconLastError ?? null,
|
|
4389
4566
|
coverage: cov ? {
|
|
4390
|
-
|
|
4391
|
-
|
|
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 },
|
|
4392
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,
|
|
4393
4578
|
}
|
|
4394
4579
|
};
|
|
4395
4580
|
}
|
|
@@ -4901,6 +5086,7 @@ async function main() {
|
|
|
4901
5086
|
// Synchronously, before anything is awaited: a host may SIGKILL before the settle
|
|
4902
5087
|
// sequence below completes (codex: ~185 ms after SIGTERM).
|
|
4903
5088
|
ragKgManager.checkpointWal();
|
|
5089
|
+
ragKgManager.noteWrites();
|
|
4904
5090
|
void (async () => {
|
|
4905
5091
|
try {
|
|
4906
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;
|