@soulcraft/cor 3.0.8 → 3.0.10
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.
|
@@ -209,6 +209,14 @@ export declare class NativeDiskAnnWrapper implements VectorIndexProvider {
|
|
|
209
209
|
private manifestNextId;
|
|
210
210
|
/** Segment-manifest cold-load memo (see {@link ensureSegments}). */
|
|
211
211
|
private segmentsLoaded;
|
|
212
|
+
/** Consecutive backpressure-flush failures (see
|
|
213
|
+
* {@link FLUSH_FAILURE_BACKPRESSURE_THRESHOLD}); reset on a clean flush. */
|
|
214
|
+
private consecutiveFlushFailures;
|
|
215
|
+
/** Consecutive background-consolidation failures + the earliest time the next
|
|
216
|
+
* attempt may fire — exponential backoff so a stranded consolidation is not
|
|
217
|
+
* re-triggered on every flush (CORTEX-CONSOLIDATION-SILENT-DROP). */
|
|
218
|
+
private consolidationFailures;
|
|
219
|
+
private consolidationCooldownUntil;
|
|
212
220
|
/**
|
|
213
221
|
* Canonical storage holds vectored nouns but NO durable vector index exists —
|
|
214
222
|
* the 7.x → 3.0 migration / restore-dir shape. Detected once per cold open by
|
|
@@ -110,6 +110,41 @@ const DELTA_HARD_CAP_BYTES = 256 * 1024 * 1024;
|
|
|
110
110
|
* consolidation reads them back via `readVectorChunk` — bounded RAM.
|
|
111
111
|
*/
|
|
112
112
|
const L0_COMPACT_THRESHOLD = 4;
|
|
113
|
+
/**
|
|
114
|
+
* Consecutive backpressure-flush failures before `addItem` stops swallowing and
|
|
115
|
+
* REFUSES writes (CORTEX-CONSOLIDATION-SILENT-DROP). A committed write must not
|
|
116
|
+
* fail on a transient post-commit blip — but if the flush PERSISTENTLY fails,
|
|
117
|
+
* the delta can never drain and accepting more writes grows it without bound
|
|
118
|
+
* until the process OOMs (which is what stranded memory's base). Past this
|
|
119
|
+
* count the brake engages: refuse loudly so the write-storm halts instead of
|
|
120
|
+
* crashing. A single successful flush resets the counter.
|
|
121
|
+
*/
|
|
122
|
+
const FLUSH_FAILURE_BACKPRESSURE_THRESHOLD = 4;
|
|
123
|
+
/**
|
|
124
|
+
* Cap on the exponential backoff between failed background consolidations
|
|
125
|
+
* (CORTEX-CONSOLIDATION-SILENT-DROP). A persistently-failing consolidation must
|
|
126
|
+
* not retry on every ~6 s flush (memory-vm hammered it for hours); back off up
|
|
127
|
+
* to this ceiling. A successful consolidation resets to zero.
|
|
128
|
+
*/
|
|
129
|
+
const CONSOLIDATION_BACKOFF_MAX_MS = 5 * 60_000;
|
|
130
|
+
/**
|
|
131
|
+
* Live-L0-segment count that triggers a loud warning: consolidation folds at
|
|
132
|
+
* {@link L0_COMPACT_THRESHOLD}, so accumulating this many segments means it
|
|
133
|
+
* has been failing for many cycles. Every query fans out across all live
|
|
134
|
+
* segments plus the base — memory-vm's incident reached 162 segments and
|
|
135
|
+
* degraded reads to 25–40 s. The warn is the early alarm; the hard cap below
|
|
136
|
+
* is the brake.
|
|
137
|
+
*/
|
|
138
|
+
const L0_SEGMENT_WARN_THRESHOLD = 32;
|
|
139
|
+
/**
|
|
140
|
+
* Live-L0-segment count at which `addItem` REFUSES writes loudly
|
|
141
|
+
* (CORTEX-CONSOLIDATION-SILENT-DROP hardening). Segments grow past the warn
|
|
142
|
+
* threshold only while consolidation persistently fails; each one adds query
|
|
143
|
+
* fan-out and RAM until reads collapse and the process OOMs. Same posture as
|
|
144
|
+
* the flush-failure brake: a loud bounded stop beats an unbounded quiet
|
|
145
|
+
* degradation. Clears itself the moment one consolidation folds the segments.
|
|
146
|
+
*/
|
|
147
|
+
const L0_SEGMENT_HARD_CAP = 256;
|
|
113
148
|
/**
|
|
114
149
|
* Storage key of the vector segment manifest (flat key — slashed keys trip
|
|
115
150
|
* the storage adapters' unknown-key warning; same lesson as the metadata
|
|
@@ -311,6 +346,14 @@ export class NativeDiskAnnWrapper {
|
|
|
311
346
|
manifestNextId = 1;
|
|
312
347
|
/** Segment-manifest cold-load memo (see {@link ensureSegments}). */
|
|
313
348
|
segmentsLoaded = false;
|
|
349
|
+
/** Consecutive backpressure-flush failures (see
|
|
350
|
+
* {@link FLUSH_FAILURE_BACKPRESSURE_THRESHOLD}); reset on a clean flush. */
|
|
351
|
+
consecutiveFlushFailures = 0;
|
|
352
|
+
/** Consecutive background-consolidation failures + the earliest time the next
|
|
353
|
+
* attempt may fire — exponential backoff so a stranded consolidation is not
|
|
354
|
+
* re-triggered on every flush (CORTEX-CONSOLIDATION-SILENT-DROP). */
|
|
355
|
+
consolidationFailures = 0;
|
|
356
|
+
consolidationCooldownUntil = 0;
|
|
314
357
|
/**
|
|
315
358
|
* Canonical storage holds vectored nouns but NO durable vector index exists —
|
|
316
359
|
* the 7.x → 3.0 migration / restore-dir shape. Detected once per cold open by
|
|
@@ -409,6 +452,18 @@ export class NativeDiskAnnWrapper {
|
|
|
409
452
|
* `rebuild()` call, which folds the delta into the main index.
|
|
410
453
|
*/
|
|
411
454
|
async addItem(item) {
|
|
455
|
+
// Segment brake (CORTEX-CONSOLIDATION-SILENT-DROP): segments accumulate
|
|
456
|
+
// past the warn threshold only while consolidation persistently fails —
|
|
457
|
+
// the delta keeps draining to NEW segments (flush works), so the flush-
|
|
458
|
+
// failure brake never engages, yet fan-out and RAM grow without bound
|
|
459
|
+
// (162 segments degraded memory-vm's reads to 25–40 s en route to OOM).
|
|
460
|
+
// Refuse loudly at the cap; one successful consolidation clears it.
|
|
461
|
+
if (this.l0Segments.length >= L0_SEGMENT_HARD_CAP) {
|
|
462
|
+
throw new Error(`NativeDiskAnnWrapper: ${this.l0Segments.length} live L0 segments — ` +
|
|
463
|
+
`consolidation is persistently failing; refusing writes to prevent ` +
|
|
464
|
+
`unbounded segment growth (read collapse / OOM). See the ` +
|
|
465
|
+
`consolidation-failure log lines for the underlying cause.`);
|
|
466
|
+
}
|
|
412
467
|
// item.vector.length is the authoritative width for THIS write; fall back to
|
|
413
468
|
// it when the brain is zero-config (config.dimensions undefined). Otherwise
|
|
414
469
|
// bytesPerVector is NaN and every `NaN >= cap` backpressure/fold check is
|
|
@@ -426,17 +481,29 @@ export class NativeDiskAnnWrapper {
|
|
|
426
481
|
// its retirement filter and the serialized manifest saves keep the
|
|
427
482
|
// on-disk list consistent).
|
|
428
483
|
//
|
|
429
|
-
// A committed write must
|
|
484
|
+
// A committed write must not fail on a TRANSIENT post-commit blip — the
|
|
430
485
|
// caller would retry and double-write (MEMORY-COR-RANGE-ERROR isolation
|
|
431
|
-
// ask)
|
|
432
|
-
//
|
|
486
|
+
// ask), so a one-off flush failure is logged and the write is accepted,
|
|
487
|
+
// delta retained. BUT this is pre-delta-append backpressure: if the flush
|
|
488
|
+
// PERSISTENTLY fails the delta can never drain, and swallowing forever
|
|
489
|
+
// grows it without bound until OOM — which is exactly what stranded
|
|
490
|
+
// memory-vm's base (CORTEX-CONSOLIDATION-SILENT-DROP). Past the threshold
|
|
491
|
+
// the brake engages: refuse loudly so the write-storm halts. Recovers
|
|
492
|
+
// itself once a flush succeeds (self-heal drops a stranded base).
|
|
433
493
|
try {
|
|
434
494
|
await (this.flushInFlight ?? this.flushDeltaToL0());
|
|
495
|
+
this.consecutiveFlushFailures = 0;
|
|
435
496
|
}
|
|
436
497
|
catch (error) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
498
|
+
this.consecutiveFlushFailures++;
|
|
499
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
500
|
+
prodLog.error(`NativeDiskAnnWrapper: backpressure flush failed ` +
|
|
501
|
+
`(${this.consecutiveFlushFailures}× consecutively): ${msg}`);
|
|
502
|
+
if (this.consecutiveFlushFailures >= FLUSH_FAILURE_BACKPRESSURE_THRESHOLD) {
|
|
503
|
+
throw new Error(`NativeDiskAnnWrapper: vector index flush has failed ` +
|
|
504
|
+
`${this.consecutiveFlushFailures}× consecutively — refusing writes to ` +
|
|
505
|
+
`prevent unbounded delta growth (OOM). Last cause: ${msg}`);
|
|
506
|
+
}
|
|
440
507
|
}
|
|
441
508
|
}
|
|
442
509
|
if (this.tombstones.has(item.id)) {
|
|
@@ -933,6 +1000,26 @@ export class NativeDiskAnnWrapper {
|
|
|
933
1000
|
if (!NativeDiskANN) {
|
|
934
1001
|
throw new Error('NativeDiskANN binding missing — rebuild requires the cor native module');
|
|
935
1002
|
}
|
|
1003
|
+
// **Self-heal a stranded base (CORTEX-CONSOLIDATION-SILENT-DROP).** We hold
|
|
1004
|
+
// `this.native` as a live mmap handle to `indexPath`. On Linux that handle
|
|
1005
|
+
// survives its file being unlinked — so if a crash interrupted a prior
|
|
1006
|
+
// consolidation mid-write (native rebuild unlinks the old base to rewrite
|
|
1007
|
+
// it), or an external move took the file, `this.native` stays truthy while
|
|
1008
|
+
// the PATH is dead. Trusting the handle then makes `rebuildFromExistingAsync`
|
|
1009
|
+
// open a path that no longer exists → `open existing index failed: No such
|
|
1010
|
+
// file or directory`, looping every trigger with segments piling up
|
|
1011
|
+
// unbounded. Drop the stale handle here so the fold below rebuilds from the
|
|
1012
|
+
// durable L0 segments + canonical (the source of truth) and re-creates the
|
|
1013
|
+
// base, instead of looping on a ghost. `existsSync` is O(1); this runs only
|
|
1014
|
+
// on the (rare) consolidation path, never per-write.
|
|
1015
|
+
if (this.native && !existsSync(this.config.indexPath)) {
|
|
1016
|
+
prodLog.error(`NativeDiskAnnWrapper: durable base index missing under a live handle ` +
|
|
1017
|
+
`(${this.config.indexPath}) — crash-stranded or externally moved. ` +
|
|
1018
|
+
`Rebuilding from segments + canonical instead of looping on the dead path.`);
|
|
1019
|
+
this.native = null;
|
|
1020
|
+
// A rebuild is about to regenerate the base; clear any failed-load flag.
|
|
1021
|
+
this.durableBaseLoadFailed = false;
|
|
1022
|
+
}
|
|
936
1023
|
// Build the new logical slot ordering: (live old slots) + (delta).
|
|
937
1024
|
// **Critical for billion-scale correctness**: the old vectors stay
|
|
938
1025
|
// mmap'd inside the native module — we only pass slot IDs across
|
|
@@ -1198,6 +1285,10 @@ export class NativeDiskAnnWrapper {
|
|
|
1198
1285
|
// The rebuild's durable output now covers canonical — the cold-open
|
|
1199
1286
|
// not-ready verdict (migration/restore dir) is satisfied.
|
|
1200
1287
|
this.canonicalUnindexed = false;
|
|
1288
|
+
// A successful fold clears the consolidation-failure backoff (any caller:
|
|
1289
|
+
// the auto-trigger, brainy's cold-open gate, or a host repairIndex).
|
|
1290
|
+
this.consolidationFailures = 0;
|
|
1291
|
+
this.consolidationCooldownUntil = 0;
|
|
1201
1292
|
}
|
|
1202
1293
|
/**
|
|
1203
1294
|
* Flush the delta buffer to disk. For DiskANN the delta is in-memory
|
|
@@ -1462,13 +1553,42 @@ export class NativeDiskAnnWrapper {
|
|
|
1462
1553
|
}
|
|
1463
1554
|
prodLog.info(`NativeDiskAnnWrapper: flushed ${builtDelta.size} delta entries to L0 segment ${file} ` +
|
|
1464
1555
|
`(${this.l0Segments.length} live segments)`);
|
|
1556
|
+
// Integrity probe (CORTEX-CONSOLIDATION-SILENT-DROP hardening): one O(1)
|
|
1557
|
+
// stat per flush catches a base deleted out from under the live handle in
|
|
1558
|
+
// SECONDS instead of at the next consolidation — the strand ran for hours
|
|
1559
|
+
// undetected on memory-vm. Detection here; the heal is the consolidation
|
|
1560
|
+
// below (runRebuild's self-heal drops the stale handle and rebuilds from
|
|
1561
|
+
// segments + canonical).
|
|
1562
|
+
const baseMissing = this.native !== null &&
|
|
1563
|
+
this.config.indexPath !== undefined &&
|
|
1564
|
+
!existsSync(this.config.indexPath);
|
|
1565
|
+
if (baseMissing) {
|
|
1566
|
+
prodLog.error(`NativeDiskAnnWrapper: durable base index missing under a live handle ` +
|
|
1567
|
+
`(${this.config.indexPath}) — triggering self-heal consolidation`);
|
|
1568
|
+
}
|
|
1569
|
+
if (this.l0Segments.length >= L0_SEGMENT_WARN_THRESHOLD) {
|
|
1570
|
+
prodLog.error(`NativeDiskAnnWrapper: ${this.l0Segments.length} live L0 segments ` +
|
|
1571
|
+
`(consolidation folds at ${L0_COMPACT_THRESHOLD}) — consolidation is ` +
|
|
1572
|
+
`failing; query fan-out is degrading and the write brake engages at ` +
|
|
1573
|
+
`${L0_SEGMENT_HARD_CAP}`);
|
|
1574
|
+
}
|
|
1465
1575
|
// Tiered maintenance: enough small segments → one BACKGROUND consolidation
|
|
1466
1576
|
// (the async rebuild folds base + L0s + delta into a fresh base off-thread).
|
|
1467
|
-
|
|
1577
|
+
// A missing base skips the segment-count threshold — heal now, not three
|
|
1578
|
+
// flushes from now. Guarded by an exponential backoff: a persistently-
|
|
1579
|
+
// failing consolidation must not re-fire on every ~6 s flush (memory-vm
|
|
1580
|
+
// hammered it for hours) — it backs off up to CONSOLIDATION_BACKOFF_MAX_MS,
|
|
1581
|
+
// and a success resets it (see the tail of runRebuild).
|
|
1582
|
+
if ((baseMissing || this.l0Segments.length >= L0_COMPACT_THRESHOLD) &&
|
|
1583
|
+
!this.rebuildInFlight &&
|
|
1584
|
+
Date.now() >= this.consolidationCooldownUntil) {
|
|
1468
1585
|
this.rebuild().catch((error) => {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1586
|
+
this.consolidationFailures++;
|
|
1587
|
+
const backoffMs = Math.min(CONSOLIDATION_BACKOFF_MAX_MS, 1000 * 2 ** Math.min(this.consolidationFailures, 8));
|
|
1588
|
+
this.consolidationCooldownUntil = Date.now() + backoffMs;
|
|
1589
|
+
prodLog.error(`NativeDiskAnnWrapper: background consolidation failed ` +
|
|
1590
|
+
`(${this.consolidationFailures}× — next attempt in ~${Math.round(backoffMs / 1000)}s; ` +
|
|
1591
|
+
`segments retained): ${error instanceof Error ? error.message : String(error)}`);
|
|
1472
1592
|
});
|
|
1473
1593
|
}
|
|
1474
1594
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/cor",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.10",
|
|
4
4
|
"description": "Native Rust acceleration for Brainy — SIMD distance, vector quantization, zero-copy mmap, native embeddings. Free tier for storage, Pro license for compute acceleration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|