@soulcraft/cor 3.0.11 → 3.0.12

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.
@@ -1030,8 +1030,33 @@ export interface NativeLsmEngineInstance {
1030
1030
  * data lives in both the log and the persisted SSTable, so the log copy is
1031
1031
  * safe to retire. No-op for in-memory engines (constructed without
1032
1032
  * `openWithDir`).
1033
+ *
1034
+ * Returns the number of records CARRIED FORWARD across the shard-log
1035
+ * rotations — writes that landed between the flush's drain snapshot and
1036
+ * this commit (the wrapper's `await`s on blob + manifest IO let the event
1037
+ * loop run writes in that window). Carried records stay replayable in the
1038
+ * fresh active logs and drain with the next flush; pre-3.0.12 rotations
1039
+ * exiled them into never-replayed archives (CORTEX-RESTART-STRAND).
1040
+ */
1041
+ commitFlushThrough(throughEpoch: bigint): number;
1042
+ /**
1043
+ * Replay accounting from the memtable's cold open, as JSON:
1044
+ * `{ activeRecords, archiveRecovered, archivesUnreadable, tornTailBytes,
1045
+ * liveLogsRecreated, anomalousShards: string[] }`. Logged loudly at init
1046
+ * when any anomaly is non-zero — the forensic record of what cold-open
1047
+ * recovery found, captured BEFORE any rebuild wipes the evidence.
1048
+ * `liveLogsRecreated > 0` or `archivesUnreadable > 0` is a strand verdict
1049
+ * (unflushed records from a missing live log are unknowable).
1050
+ */
1051
+ replayStatsJson(): string;
1052
+ /**
1053
+ * Toggle bulk-load mode on the shard logs. `true` suspends the
1054
+ * per-append msyncs — rebuild-from-canonical ONLY (canonical is the
1055
+ * durability source; a crashed rebuild re-runs). `false` restores them
1056
+ * and performs one full msync per shard: the rebuild's single durability
1057
+ * point. Never used on the live write path.
1033
1058
  */
1034
- commitFlushThrough(throughEpoch: bigint): void;
1059
+ setBulkLoad(on: boolean): void;
1035
1060
  /**
1036
1061
  * Update the budget ceiling. Driven by the wrapper's
1037
1062
  * `ResourceManager` subscriber callback. Pass `0xffffffffffffffffn`
@@ -889,6 +889,15 @@ export declare class MetadataIndexManager implements MetadataIndexProvider {
889
889
  * @returns the number of entities indexed (those carrying metadata).
890
890
  */
891
891
  private rebuildEntityMetadata;
892
+ /**
893
+ * Rebuild-path verify-read for a single entity's canonical metadata.
894
+ * Retries once on a thrown read, then rethrows — a rebuild must NEVER
895
+ * silently exclude an entity because a storage read faulted (that
896
+ * completes the rebuild under-posted and clears the strand verdict
897
+ * over missing data). A clean `null`/`undefined` result is returned
898
+ * as `null` — a genuinely metadata-less record, the caller counts it.
899
+ */
900
+ private readRebuildMetadataOrThrow;
892
901
  /**
893
902
  * Batch-load metadata for a page of noun/verb ids — prefers the storage
894
903
  * adapter's batch API, falls back to per-id reads.
@@ -487,6 +487,35 @@ export class MetadataIndexManager {
487
487
  engine.registerSstablePath(path);
488
488
  }
489
489
  }
490
+ // Cold-open replay forensics (CORTEX-RESTART-STRAND). The engine's
491
+ // open replayed the active shard logs AND recovered any records a
492
+ // prior session exiled into archives (pre-carry-forward rotations,
493
+ // mid-rotation crashes). Surface every anomaly LOUDLY here — this
494
+ // line is the evidence that survives even if a rebuild later wipes
495
+ // the on-disk state.
496
+ if (metaDir !== null && typeof this.lsmEngine.replayStatsJson === 'function') {
497
+ try {
498
+ const stats = JSON.parse(this.lsmEngine.replayStatsJson());
499
+ if (stats.archiveRecovered > 0 ||
500
+ stats.archivesUnreadable > 0 ||
501
+ stats.tornTailBytes > 0) {
502
+ prodLog.error(`[NativeMetadataIndex] COLD-OPEN RECOVERY: replayed ` +
503
+ `${stats.activeRecords} active-log record(s); RECOVERED ` +
504
+ `${stats.archiveRecovered} record(s) exiled into archives by a ` +
505
+ `prior session; ${stats.archivesUnreadable} unreadable ` +
506
+ `archive(s); ${stats.tornTailBytes} torn-tail byte(s). ` +
507
+ `Recovered records are served and drain with the next flush. ` +
508
+ `Per-shard: ${stats.anomalousShards.join(' | ')}`);
509
+ }
510
+ }
511
+ catch (error) {
512
+ prodLog.warn('[NativeMetadataIndex] replay-forensics read failed (non-fatal):', error);
513
+ }
514
+ }
515
+ else if (metaDir !== null) {
516
+ prodLog.warn('[NativeMetadataIndex] replay forensics unavailable — the loaded ' +
517
+ 'native binary predates 3.0.12; upgrade the paired .node build.');
518
+ }
490
519
  }
491
520
  /**
492
521
  * **LSM durability (steps 3-4) — the sync part.** Register the
@@ -766,7 +795,18 @@ export class MetadataIndexManager {
766
795
  // flushed data is safely in the new SSTable. Warn + continue rather than
767
796
  // abort the rebuild — the next clean flush retires the log.
768
797
  try {
769
- engine.commitFlushThrough(result.lastDurableEpoch);
798
+ const carried = engine.commitFlushThrough(result.lastDurableEpoch);
799
+ // Writes that landed during THIS flush's blob/manifest awaits (the
800
+ // event loop runs between the drain and this commit) were carried
801
+ // into the fresh active logs — they stay replayable and drain with
802
+ // the next flush. Logged for the write-ack audit trail: pre-3.0.12
803
+ // rotations exiled exactly these records into never-replayed
804
+ // archives (CORTEX-RESTART-STRAND).
805
+ if (typeof carried === 'number' && carried > 0) {
806
+ prodLog.info(`flushLsm: carried ${carried} concurrent-write record(s) across ` +
807
+ `the shard-log rotation for SSTable ${id} — they drain with the ` +
808
+ `next flush.`);
809
+ }
770
810
  }
771
811
  catch (error) {
772
812
  prodLog.warn(`flushLsm: commitFlushThrough deferred for SSTable ${id} (it + the ` +
@@ -1197,6 +1237,36 @@ export class MetadataIndexManager {
1197
1237
  async detectDerivedStrand() {
1198
1238
  if (!this.lsmEngine || !this.lsmMetaDir)
1199
1239
  return;
1240
+ // 0. Replay-stats verdict (3.0.12). The native open now RECREATES a
1241
+ // missing live log and recovers what its archives hold, so the
1242
+ // filename census below can no longer see the deleted-live-log
1243
+ // signature — the native layer reports it instead. Either condition
1244
+ // is a strand verdict: a recreated-with-archives shard may have held
1245
+ // UNFLUSHED records that lived only in the missing live log
1246
+ // (unknowable — rebuild is the only honest answer), and an
1247
+ // unreadable archive means potentially-exiled records were
1248
+ // unreachable to recovery.
1249
+ try {
1250
+ const engine = this.lsmEngine;
1251
+ if (typeof engine.replayStatsJson === 'function') {
1252
+ const stats = JSON.parse(engine.replayStatsJson());
1253
+ const recreated = stats.liveLogsRecreated ?? 0;
1254
+ const unreadable = stats.archivesUnreadable ?? 0;
1255
+ if (recreated > 0 || unreadable > 0) {
1256
+ this.strandDetected = true;
1257
+ prodLog.error(`[NativeMetadataIndex] STRAND: cold-open replay reports ` +
1258
+ `${recreated} shard live log(s) MISSING (recreated; archives ` +
1259
+ `recovered what they held, but unflushed records from the ` +
1260
+ `missing logs are unknowable) and ${unreadable} unreadable ` +
1261
+ `archive(s). Reporting not-ready so the cold-open gate ` +
1262
+ `rebuilds from canonical. Per-shard: ` +
1263
+ `${(stats.anomalousShards ?? []).join(' | ')}`);
1264
+ }
1265
+ }
1266
+ }
1267
+ catch {
1268
+ // Verdict probe is advisory — a stats read failure must not block open.
1269
+ }
1200
1270
  // 1. Archive-orphan census over the memtable shard logs.
1201
1271
  try {
1202
1272
  const memtableDir = join(this.lsmMetaDir, 'memtable');
@@ -2584,8 +2654,31 @@ export class MetadataIndexManager {
2584
2654
  // branch loaded the whole corpus in a single getNouns/getVerbs({limit:1e6})
2585
2655
  // call (cap 1M), and the cloud branch capped at MAX_ITERATIONS(10000)×
2586
2656
  // batch(25) ≈ 250K. (#74 — perf-at-all-scales mandate.)
2587
- const totalNounsProcessed = await this.rebuildEntityMetadata('noun');
2588
- const totalVerbsProcessed = await this.rebuildEntityMetadata('verb');
2657
+ // Bulk-load mode for the re-post walk: canonical is the durability
2658
+ // source during a rebuild (a crash re-runs it), so the per-append
2659
+ // shard-log msyncs — ~2 synchronous disk flushes per posting record,
2660
+ // the mechanism behind memory-vm's MEASURED ~1.3-2 items/s (30 min
2661
+ // for 2,354 entities on e2-standard-2, CORTEX-RESTART-STRAND) — are
2662
+ // suspended and replaced by ONE full msync per shard at the end of
2663
+ // the walk (setBulkLoad(false), inside finally so a failed rebuild
2664
+ // can never leave the live write path in deferred mode).
2665
+ const bulkCapable = this.lsmEngine !== null &&
2666
+ typeof this.lsmEngine.setBulkLoad ===
2667
+ 'function';
2668
+ if (bulkCapable)
2669
+ this.lsmEngine.setBulkLoad(true);
2670
+ let totalNounsProcessed;
2671
+ let totalVerbsProcessed;
2672
+ try {
2673
+ totalNounsProcessed = await this.rebuildEntityMetadata('noun');
2674
+ totalVerbsProcessed = await this.rebuildEntityMetadata('verb');
2675
+ }
2676
+ finally {
2677
+ // Leaving bulk mode performs the walk's single durability point
2678
+ // (one full msync per shard log).
2679
+ if (bulkCapable)
2680
+ this.lsmEngine.setBulkLoad(false);
2681
+ }
2589
2682
  // Final flush
2590
2683
  await this.flushRebuildDirty();
2591
2684
  await this.flush();
@@ -2616,6 +2709,7 @@ export class MetadataIndexManager {
2616
2709
  let hasMore = true;
2617
2710
  let processed = 0;
2618
2711
  let sinceFlush = 0;
2712
+ let noMetadata = 0;
2619
2713
  while (hasMore) {
2620
2714
  const page = kind === 'noun'
2621
2715
  ? await this.storage.getNouns({ pagination: { limit: PAGE, cursor } })
@@ -2626,7 +2720,16 @@ export class MetadataIndexManager {
2626
2720
  const ids = items.map((it) => it.id);
2627
2721
  const md = await this.loadRebuildMetadataBatch(kind, ids);
2628
2722
  for (const it of items) {
2629
- const metadata = md.get(it.id);
2723
+ // An id enumerated by canonical but absent from the batch result
2724
+ // gets an individual verify-read: a batch adapter may return a
2725
+ // partial map on an internal fault, and silently skipping the
2726
+ // entity would complete the rebuild UNDER-POSTED — the exact
2727
+ // damage the strand detector exists to catch, laundered by its
2728
+ // own healer (the 01:31 memory-vm rebuild's failure shape). The
2729
+ // verify-read retries once, then FAILS THE REBUILD loudly; a
2730
+ // clean null (a genuinely metadata-less record) is counted +
2731
+ // reported, never silently dropped.
2732
+ const metadata = md.get(it.id) ?? (await this.readRebuildMetadataOrThrow(kind, it.id));
2630
2733
  if (metadata) {
2631
2734
  await this.addToIndex(it.id, metadata, true, true);
2632
2735
  processed++;
@@ -2635,13 +2738,49 @@ export class MetadataIndexManager {
2635
2738
  sinceFlush = 0;
2636
2739
  }
2637
2740
  }
2741
+ else {
2742
+ noMetadata++;
2743
+ }
2638
2744
  }
2639
2745
  hasMore = page.hasMore;
2640
2746
  cursor = page.nextCursor;
2641
2747
  await this.yieldToEventLoop();
2642
2748
  }
2749
+ if (noMetadata > 0) {
2750
+ prodLog.warn(`[NativeMetadataIndex] rebuild(${kind}): ${noMetadata} enumerated ` +
2751
+ `entit${noMetadata === 1 ? 'y' : 'ies'} carried no canonical ` +
2752
+ `metadata (verified by direct read) and were not indexed — ` +
2753
+ `expected only for metadata-less legacy records.`);
2754
+ }
2643
2755
  return processed;
2644
2756
  }
2757
+ /**
2758
+ * Rebuild-path verify-read for a single entity's canonical metadata.
2759
+ * Retries once on a thrown read, then rethrows — a rebuild must NEVER
2760
+ * silently exclude an entity because a storage read faulted (that
2761
+ * completes the rebuild under-posted and clears the strand verdict
2762
+ * over missing data). A clean `null`/`undefined` result is returned
2763
+ * as `null` — a genuinely metadata-less record, the caller counts it.
2764
+ */
2765
+ async readRebuildMetadataOrThrow(kind, id) {
2766
+ const read = () => kind === 'noun'
2767
+ ? this.storage.getNounMetadata(id)
2768
+ : this.storage.getVerbMetadata(id);
2769
+ try {
2770
+ return (await read()) ?? null;
2771
+ }
2772
+ catch {
2773
+ try {
2774
+ return (await read()) ?? null;
2775
+ }
2776
+ catch (error) {
2777
+ throw new Error(`[NativeMetadataIndex] rebuild(${kind}): canonical metadata read ` +
2778
+ `for '${id}' faulted twice — failing the rebuild rather than ` +
2779
+ `silently excluding the entity (an under-posted rebuild is the ` +
2780
+ `strand bug reintroduced by its own healer). Cause: ${String(error)}`);
2781
+ }
2782
+ }
2783
+ }
2645
2784
  /**
2646
2785
  * Batch-load metadata for a page of noun/verb ids — prefers the storage
2647
2786
  * adapter's batch API, falls back to per-id reads.
@@ -2656,14 +2795,13 @@ export class MetadataIndexManager {
2656
2795
  }
2657
2796
  const m = new Map();
2658
2797
  for (const id of ids) {
2659
- try {
2660
- const md = kind === 'noun'
2661
- ? await this.storage.getNounMetadata(id)
2662
- : await this.storage.getVerbMetadata(id);
2663
- if (md)
2664
- m.set(id, md);
2665
- }
2666
- catch { }
2798
+ // Retry-once-then-throw (readRebuildMetadataOrThrow): a faulted
2799
+ // read must fail the rebuild loudly, never silently exclude the
2800
+ // entity — a swallowed fault here completed the rebuild
2801
+ // under-posted (the strand bug re-created by its own healer).
2802
+ const md = await this.readRebuildMetadataOrThrow(kind, id);
2803
+ if (md)
2804
+ m.set(id, md);
2667
2805
  }
2668
2806
  return m;
2669
2807
  }
@@ -128,7 +128,13 @@ is the reference implementation.
128
128
  tail offset; live writers append past that offset without
129
129
  affecting what the snapshot reader sees. On compaction
130
130
  commit the delta is renamed to a `.archive` sibling and a
131
- fresh empty delta replaces it.
131
+ fresh delta replaces it, carrying forward any record newer
132
+ than the commit watermark — a record appended while the
133
+ commit's durability IO was in flight stays replayable in the
134
+ fresh delta, never exiled to the archive. Opens also scan
135
+ retained archives for records above the watermark and
136
+ recover them, so even a rotation interrupted between its two
137
+ renames loses nothing.
132
138
 
133
139
  3. **Atomic head pointer** (`head`) — a tiny text file named by
134
140
  `PointerFile` (`diskann::pointer_file`). Names the current
package/native/index.d.ts CHANGED
@@ -1260,8 +1260,34 @@ export declare class NativeLsmEngine {
1260
1260
  *
1261
1261
  * No-op for in-memory engines (constructed without
1262
1262
  * `openWithDir`).
1263
- */
1264
- commitFlushThrough(throughEpoch: bigint): void
1263
+ *
1264
+ * Returns the number of records carried forward across the
1265
+ * shard-log rotations — writes that landed between the flush's
1266
+ * drain snapshot and this commit (the wrapper's async
1267
+ * durability-IO window; the JS event loop runs writes there).
1268
+ * They stay replayable in the fresh active logs and drain with
1269
+ * the next flush. The wrapper logs non-zero counts for the
1270
+ * write-ack audit trail.
1271
+ */
1272
+ commitFlushThrough(throughEpoch: bigint): number
1273
+ /**
1274
+ * Toggle bulk-load mode on the shard logs. `true` suspends the
1275
+ * per-append msyncs — for rebuild-from-canonical ONLY (canonical
1276
+ * is the durability source; a crashed rebuild re-runs). `false`
1277
+ * restores them and performs one full msync per shard: the
1278
+ * rebuild's single durability point. Never used on the live
1279
+ * write path, whose ack contract requires the per-append msync.
1280
+ */
1281
+ setBulkLoad(on: boolean): void
1282
+ /**
1283
+ * Replay accounting from the memtable's cold open, as JSON:
1284
+ * `{ activeRecords, archiveRecovered, archivesUnreadable,
1285
+ * tornTailBytes, anomalousShards: string[] }`. The wrapper logs
1286
+ * this at init when any anomaly is non-zero — the forensic
1287
+ * record of what cold-open recovery found, captured BEFORE any
1288
+ * rebuild wipes the evidence (CORTEX-RESTART-STRAND).
1289
+ */
1290
+ replayStatsJson(): string
1265
1291
  /** `"u32"` or `"u64"`. */
1266
1292
  idSpace(): string
1267
1293
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/cor",
3
- "version": "3.0.11",
3
+ "version": "3.0.12",
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",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "devDependencies": {
84
84
  "@napi-rs/cli": "^3.0.0",
85
- "@soulcraft/brainy": "8.2.2",
85
+ "@soulcraft/brainy": "8.2.5",
86
86
  "@types/node": "^22.0.0",
87
87
  "pg": "^8.21.0",
88
88
  "tsx": "^4.21.0",