@soulcraft/cor 3.0.8 → 3.0.9

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,23 @@ 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;
113
130
  /**
114
131
  * Storage key of the vector segment manifest (flat key — slashed keys trip
115
132
  * the storage adapters' unknown-key warning; same lesson as the metadata
@@ -311,6 +328,14 @@ export class NativeDiskAnnWrapper {
311
328
  manifestNextId = 1;
312
329
  /** Segment-manifest cold-load memo (see {@link ensureSegments}). */
313
330
  segmentsLoaded = false;
331
+ /** Consecutive backpressure-flush failures (see
332
+ * {@link FLUSH_FAILURE_BACKPRESSURE_THRESHOLD}); reset on a clean flush. */
333
+ consecutiveFlushFailures = 0;
334
+ /** Consecutive background-consolidation failures + the earliest time the next
335
+ * attempt may fire — exponential backoff so a stranded consolidation is not
336
+ * re-triggered on every flush (CORTEX-CONSOLIDATION-SILENT-DROP). */
337
+ consolidationFailures = 0;
338
+ consolidationCooldownUntil = 0;
314
339
  /**
315
340
  * Canonical storage holds vectored nouns but NO durable vector index exists —
316
341
  * the 7.x → 3.0 migration / restore-dir shape. Detected once per cold open by
@@ -426,17 +451,29 @@ export class NativeDiskAnnWrapper {
426
451
  // its retirement filter and the serialized manifest saves keep the
427
452
  // on-disk list consistent).
428
453
  //
429
- // A committed write must NEVER fail on post-commit index maintenance — the
454
+ // A committed write must not fail on a TRANSIENT post-commit blip — the
430
455
  // caller would retry and double-write (MEMORY-COR-RANGE-ERROR isolation
431
- // ask). A backpressure-flush failure is a loud alarm, not a rejected
432
- // write: log it, keep the delta for the next trigger, accept the write.
456
+ // ask), so a one-off flush failure is logged and the write is accepted,
457
+ // delta retained. BUT this is pre-delta-append backpressure: if the flush
458
+ // PERSISTENTLY fails the delta can never drain, and swallowing forever
459
+ // grows it without bound until OOM — which is exactly what stranded
460
+ // memory-vm's base (CORTEX-CONSOLIDATION-SILENT-DROP). Past the threshold
461
+ // the brake engages: refuse loudly so the write-storm halts. Recovers
462
+ // itself once a flush succeeds (self-heal drops a stranded base).
433
463
  try {
434
464
  await (this.flushInFlight ?? this.flushDeltaToL0());
465
+ this.consecutiveFlushFailures = 0;
435
466
  }
436
467
  catch (error) {
437
- prodLog.error(`NativeDiskAnnWrapper: backpressure flush failed (write accepted; ` +
438
- `delta retained for retry): ` +
439
- `${error instanceof Error ? error.message : String(error)}`);
468
+ this.consecutiveFlushFailures++;
469
+ const msg = error instanceof Error ? error.message : String(error);
470
+ prodLog.error(`NativeDiskAnnWrapper: backpressure flush failed ` +
471
+ `(${this.consecutiveFlushFailures}× consecutively): ${msg}`);
472
+ if (this.consecutiveFlushFailures >= FLUSH_FAILURE_BACKPRESSURE_THRESHOLD) {
473
+ throw new Error(`NativeDiskAnnWrapper: vector index flush has failed ` +
474
+ `${this.consecutiveFlushFailures}× consecutively — refusing writes to ` +
475
+ `prevent unbounded delta growth (OOM). Last cause: ${msg}`);
476
+ }
440
477
  }
441
478
  }
442
479
  if (this.tombstones.has(item.id)) {
@@ -933,6 +970,26 @@ export class NativeDiskAnnWrapper {
933
970
  if (!NativeDiskANN) {
934
971
  throw new Error('NativeDiskANN binding missing — rebuild requires the cor native module');
935
972
  }
973
+ // **Self-heal a stranded base (CORTEX-CONSOLIDATION-SILENT-DROP).** We hold
974
+ // `this.native` as a live mmap handle to `indexPath`. On Linux that handle
975
+ // survives its file being unlinked — so if a crash interrupted a prior
976
+ // consolidation mid-write (native rebuild unlinks the old base to rewrite
977
+ // it), or an external move took the file, `this.native` stays truthy while
978
+ // the PATH is dead. Trusting the handle then makes `rebuildFromExistingAsync`
979
+ // open a path that no longer exists → `open existing index failed: No such
980
+ // file or directory`, looping every trigger with segments piling up
981
+ // unbounded. Drop the stale handle here so the fold below rebuilds from the
982
+ // durable L0 segments + canonical (the source of truth) and re-creates the
983
+ // base, instead of looping on a ghost. `existsSync` is O(1); this runs only
984
+ // on the (rare) consolidation path, never per-write.
985
+ if (this.native && !existsSync(this.config.indexPath)) {
986
+ prodLog.error(`NativeDiskAnnWrapper: durable base index missing under a live handle ` +
987
+ `(${this.config.indexPath}) — crash-stranded or externally moved. ` +
988
+ `Rebuilding from segments + canonical instead of looping on the dead path.`);
989
+ this.native = null;
990
+ // A rebuild is about to regenerate the base; clear any failed-load flag.
991
+ this.durableBaseLoadFailed = false;
992
+ }
936
993
  // Build the new logical slot ordering: (live old slots) + (delta).
937
994
  // **Critical for billion-scale correctness**: the old vectors stay
938
995
  // mmap'd inside the native module — we only pass slot IDs across
@@ -1198,6 +1255,10 @@ export class NativeDiskAnnWrapper {
1198
1255
  // The rebuild's durable output now covers canonical — the cold-open
1199
1256
  // not-ready verdict (migration/restore dir) is satisfied.
1200
1257
  this.canonicalUnindexed = false;
1258
+ // A successful fold clears the consolidation-failure backoff (any caller:
1259
+ // the auto-trigger, brainy's cold-open gate, or a host repairIndex).
1260
+ this.consolidationFailures = 0;
1261
+ this.consolidationCooldownUntil = 0;
1201
1262
  }
1202
1263
  /**
1203
1264
  * Flush the delta buffer to disk. For DiskANN the delta is in-memory
@@ -1464,11 +1525,20 @@ export class NativeDiskAnnWrapper {
1464
1525
  `(${this.l0Segments.length} live segments)`);
1465
1526
  // Tiered maintenance: enough small segments → one BACKGROUND consolidation
1466
1527
  // (the async rebuild folds base + L0s + delta into a fresh base off-thread).
1467
- if (this.l0Segments.length >= L0_COMPACT_THRESHOLD && !this.rebuildInFlight) {
1528
+ // Guarded by an exponential backoff: a persistently-failing consolidation
1529
+ // must not re-fire on every ~6 s flush (memory-vm hammered it for hours) —
1530
+ // it backs off up to CONSOLIDATION_BACKOFF_MAX_MS, and a success resets it
1531
+ // (see the tail of runRebuild). CORTEX-CONSOLIDATION-SILENT-DROP.
1532
+ if (this.l0Segments.length >= L0_COMPACT_THRESHOLD &&
1533
+ !this.rebuildInFlight &&
1534
+ Date.now() >= this.consolidationCooldownUntil) {
1468
1535
  this.rebuild().catch((error) => {
1469
- prodLog.error(`NativeDiskAnnWrapper: background consolidation failed (segments ` +
1470
- `retained; retries at the next trigger): ` +
1471
- `${error instanceof Error ? error.message : String(error)}`);
1536
+ this.consolidationFailures++;
1537
+ const backoffMs = Math.min(CONSOLIDATION_BACKOFF_MAX_MS, 1000 * 2 ** Math.min(this.consolidationFailures, 8));
1538
+ this.consolidationCooldownUntil = Date.now() + backoffMs;
1539
+ prodLog.error(`NativeDiskAnnWrapper: background consolidation failed ` +
1540
+ `(${this.consolidationFailures}× — next attempt in ~${Math.round(backoffMs / 1000)}s; ` +
1541
+ `segments retained): ${error instanceof Error ? error.message : String(error)}`);
1472
1542
  });
1473
1543
  }
1474
1544
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/cor",
3
- "version": "3.0.8",
3
+ "version": "3.0.9",
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",