@soulcraft/cor 3.0.21 → 3.0.22
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/dist/utils/NativeMetadataIndex.js +158 -3
- package/dist/utils/generationWatermark.js +25 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/brainy-native.node +0 -0
- package/native/index.d.ts +10 -4
- package/package.json +2 -2
|
@@ -1145,28 +1145,52 @@ export class MetadataIndexManager {
|
|
|
1145
1145
|
// Initialization
|
|
1146
1146
|
// ==========================================================================
|
|
1147
1147
|
async init() {
|
|
1148
|
+
// Init-phase narration (loop-termination doctrine): every phase of
|
|
1149
|
+
// open stamps its elapsed time on the ops channel, so a slow or
|
|
1150
|
+
// wedged boot names its own phase instead of presenting as a silent
|
|
1151
|
+
// hang after the last incidental log line (the 2026-07-18 incident
|
|
1152
|
+
// burned two gate-hours localizing a freeze that one of these
|
|
1153
|
+
// lines would have named instantly). Threshold-gated: a phase
|
|
1154
|
+
// faster than INIT_PHASE_NARRATE_MS stays silent — narration in
|
|
1155
|
+
// proportion to the work.
|
|
1156
|
+
const initStarted = Date.now();
|
|
1157
|
+
let phaseStarted = initStarted;
|
|
1158
|
+
const INIT_PHASE_NARRATE_MS = 1000;
|
|
1159
|
+
const phase = (name) => {
|
|
1160
|
+
const now = Date.now();
|
|
1161
|
+
if (now - phaseStarted >= INIT_PHASE_NARRATE_MS) {
|
|
1162
|
+
opsLine(`init phase '${name}' took ${((now - phaseStarted) / 1000).toFixed(1)}s ` +
|
|
1163
|
+
`(${((now - initStarted) / 1000).toFixed(1)}s total)`);
|
|
1164
|
+
}
|
|
1165
|
+
phaseStarted = now;
|
|
1166
|
+
};
|
|
1148
1167
|
// **Piece 11 closeout.** Detect cortex 2.x legacy JSON metadata state on
|
|
1149
1168
|
// disk. The cor 3.0 LSM engine is the only metadata engine, so an
|
|
1150
1169
|
// un-migrated 2.x brain would have its on-disk JSON state shadowed and
|
|
1151
1170
|
// surface as a mysteriously-empty brain on first query. Throw a clear
|
|
1152
1171
|
// migration error instead so the operator runs the migration script.
|
|
1153
1172
|
await this.detectLegacyJsonStateAndGate();
|
|
1173
|
+
phase('legacy-gate');
|
|
1154
1174
|
// Load field registry to discover persisted indices
|
|
1155
1175
|
await this.loadFieldRegistry();
|
|
1176
|
+
phase('field-registry');
|
|
1156
1177
|
// #72 Phase C: build the shared mmap BinaryIdMapper from the brain's
|
|
1157
1178
|
// `_id_mapper/*` paths and inject it as the core's canonical
|
|
1158
1179
|
// UUID ↔ entity-int allocator. Must run before columnStore.init (which
|
|
1159
1180
|
// resolves ids through getIdMapper()) and before any mutation.
|
|
1160
1181
|
this.wireIdMapper();
|
|
1182
|
+
phase('id-mapper');
|
|
1161
1183
|
// ADR-004 §7: declare this provider's protected families (feature-
|
|
1162
1184
|
// detected on brainy ≥8.3.0; idempotent upsert; loud-but-non-blocking).
|
|
1163
1185
|
await declareDerivedFamilies(this.storage, METADATA_FAMILIES);
|
|
1186
|
+
phase('family-declarations');
|
|
1164
1187
|
// LSM durability (steps 3-4): open the native LSM engine here (not in the
|
|
1165
1188
|
// constructor) so it can be seeded from the on-disk manifest and cold-load
|
|
1166
1189
|
// every persisted SSTable into its live read set — the entire restore,
|
|
1167
1190
|
// with NO O(N) rebuild. Runs after wireIdMapper (the engine's posting
|
|
1168
1191
|
// lists reference the mapper's entity ints) and before any read/mutation.
|
|
1169
1192
|
await this.openLsmEngine();
|
|
1193
|
+
phase('lsm-open');
|
|
1170
1194
|
// LOST-MAPPER GUARD (MEMORY-COR-IDMAP-MIGRATION, root-caused
|
|
1171
1195
|
// 2026-07-14): the ONE genuinely dangerous open-state — the mmap
|
|
1172
1196
|
// mapper was just created EMPTY while durable int-keyed postings
|
|
@@ -1205,17 +1229,20 @@ export class MetadataIndexManager {
|
|
|
1205
1229
|
// or an absent/corrupt marker) does any further work. See
|
|
1206
1230
|
// {@link guardIndexEpoch}.
|
|
1207
1231
|
await this.guardIndexEpoch();
|
|
1232
|
+
phase('epoch-guard');
|
|
1208
1233
|
// Initialize the column store with the shared storage + id mapper façade.
|
|
1209
1234
|
// Discovers any persisted `_column_index/` segments and re-applies tombstones.
|
|
1210
1235
|
// Done before the rebuild branch below so that a rebuild repopulates a
|
|
1211
1236
|
// freshly-initialized column store via addToIndex.
|
|
1212
1237
|
await this.columnStore.init(this.storage, this.getIdMapper());
|
|
1238
|
+
phase('column-store');
|
|
1213
1239
|
const hasFields = this.knownFields.size > 0;
|
|
1214
1240
|
if (!hasFields) {
|
|
1215
1241
|
// Don't trust "empty" — field registry may be missing due to interrupted flush.
|
|
1216
1242
|
// Probe storage for actual entities before concluding the workspace is empty.
|
|
1217
1243
|
try {
|
|
1218
1244
|
const probe = await this.storage.getNouns({ pagination: { limit: 1, offset: 0 } });
|
|
1245
|
+
phase('entity-probe');
|
|
1219
1246
|
const hasEntities = (probe.totalCount ?? 0) > 0 || probe.items.length > 0;
|
|
1220
1247
|
if (hasEntities) {
|
|
1221
1248
|
// **LSM durability (step 6) — rebuild demoted to disaster-recovery.**
|
|
@@ -1239,6 +1266,7 @@ export class MetadataIndexManager {
|
|
|
1239
1266
|
// lines). The strand check is boot-path-invariant: it runs
|
|
1240
1267
|
// here too, with the same verified-unpostable allowance.
|
|
1241
1268
|
await this.detectDerivedStrand();
|
|
1269
|
+
phase('strand-detect');
|
|
1242
1270
|
this.readinessInitialized = true;
|
|
1243
1271
|
return;
|
|
1244
1272
|
}
|
|
@@ -1266,8 +1294,10 @@ export class MetadataIndexManager {
|
|
|
1266
1294
|
catch {
|
|
1267
1295
|
// marker read failed — fall through to the normal rebuild path
|
|
1268
1296
|
}
|
|
1297
|
+
phase('format-marker');
|
|
1269
1298
|
console.warn(`[NativeMetadataIndex] Field registry missing but ${probe.totalCount ?? 'unknown'} entities exist — rebuilding index`);
|
|
1270
1299
|
await this.rebuild();
|
|
1300
|
+
phase('rebuild');
|
|
1271
1301
|
// A completed rebuild IS the readiness determination for this
|
|
1272
1302
|
// open (sweep finding: these early exits previously left
|
|
1273
1303
|
// readinessInitialized false — a fresh-rebuilt brain reported
|
|
@@ -1287,7 +1317,9 @@ export class MetadataIndexManager {
|
|
|
1287
1317
|
return;
|
|
1288
1318
|
}
|
|
1289
1319
|
await this.warmCache();
|
|
1320
|
+
phase('warm-cache');
|
|
1290
1321
|
await this.lazyLoadCounts();
|
|
1322
|
+
phase('lazy-counts');
|
|
1291
1323
|
this.syncTypeCountsToFixed();
|
|
1292
1324
|
// Strand detection (CORTEX-CONSOLIDATION-SILENT-DROP): the LSM can load
|
|
1293
1325
|
// "durable state" that is silently MISSING postings — deleted shard logs
|
|
@@ -1296,6 +1328,7 @@ export class MetadataIndexManager {
|
|
|
1296
1328
|
// contract reports ready, so brainy's gate heals via rebuild-from-
|
|
1297
1329
|
// canonical instead of trusting a partial index.
|
|
1298
1330
|
await this.detectDerivedStrand();
|
|
1331
|
+
phase('strand-detect');
|
|
1299
1332
|
// **#18 migration kick — LAST, after the index is fully constructed.** If the
|
|
1300
1333
|
// epoch guard flagged a large stale brain (outcome 4), start the coordinated
|
|
1301
1334
|
// blocking migration NOW: `start()` flips the lock (`isMigrating()` → true)
|
|
@@ -1327,6 +1360,7 @@ export class MetadataIndexManager {
|
|
|
1327
1360
|
this.lostMapperHeal = false;
|
|
1328
1361
|
if (coordinator) {
|
|
1329
1362
|
await coordinator.start();
|
|
1363
|
+
phase('lost-mapper-heal');
|
|
1330
1364
|
}
|
|
1331
1365
|
// No coordinator (raw/test usage): strandDetected already gates
|
|
1332
1366
|
// the metadata leg; the loud LOST-MAPPER ops line stands.
|
|
@@ -1434,6 +1468,20 @@ export class MetadataIndexManager {
|
|
|
1434
1468
|
async detectDerivedStrand() {
|
|
1435
1469
|
if (!this.lsmEngine || !this.lsmMetaDir)
|
|
1436
1470
|
return;
|
|
1471
|
+
// Stage narration (loop-termination doctrine, same contract as init's
|
|
1472
|
+
// phase stamps): every stage of this detector that touches storage or
|
|
1473
|
+
// the fact log stamps its elapsed time on the ops channel when it
|
|
1474
|
+
// exceeds 1s, so a slow open names its stage instead of presenting as
|
|
1475
|
+
// a silent hang after init's 'column-store' line. Threshold-gated —
|
|
1476
|
+
// narration in proportion to the work.
|
|
1477
|
+
let stageStarted = Date.now();
|
|
1478
|
+
const stage = (name) => {
|
|
1479
|
+
const now = Date.now();
|
|
1480
|
+
if (now - stageStarted >= 1000) {
|
|
1481
|
+
opsLine(`strand-detect stage '${name}' took ${((now - stageStarted) / 1000).toFixed(1)}s`);
|
|
1482
|
+
}
|
|
1483
|
+
stageStarted = now;
|
|
1484
|
+
};
|
|
1437
1485
|
// M3 PD-4 (Stage 1): a coherent-but-BEHIND family stamp means the
|
|
1438
1486
|
// postings simply predate the newest commits — replay EXACTLY the gap
|
|
1439
1487
|
// from the generation fact log before any strand verdict, so the
|
|
@@ -1444,18 +1492,26 @@ export class MetadataIndexManager {
|
|
|
1444
1492
|
try {
|
|
1445
1493
|
const fns = await familyStampFns();
|
|
1446
1494
|
const head = await readCommittedGeneration(this.storage);
|
|
1495
|
+
stage('replay-precheck');
|
|
1447
1496
|
if (fns && head !== undefined) {
|
|
1448
1497
|
const stamp = (await fns.readFamilyStamp(this.storage, METADATA_STAMP_PATH));
|
|
1449
1498
|
const src = stamp?.family === METADATA_FAMILY ? stamp.sourceGeneration : undefined;
|
|
1499
|
+
stage('stamp-read');
|
|
1450
1500
|
if (typeof src === 'number' && src < head) {
|
|
1451
|
-
|
|
1501
|
+
const replayed = await this.replayFactGap(src + 1);
|
|
1502
|
+
stage('fact-replay');
|
|
1503
|
+
if (replayed) {
|
|
1452
1504
|
await this.persistMetadataStamp();
|
|
1505
|
+
stage('stamp-persist');
|
|
1453
1506
|
}
|
|
1454
1507
|
}
|
|
1455
1508
|
}
|
|
1456
1509
|
}
|
|
1457
1510
|
catch {
|
|
1458
|
-
// Detector below stays authoritative.
|
|
1511
|
+
// Detector below stays authoritative. The stage marker still fires
|
|
1512
|
+
// so a slow-then-faulted stamp read/replay is attributed HERE, not
|
|
1513
|
+
// silently rolled into the next stage's elapsed time.
|
|
1514
|
+
stage('m3-replay-fault');
|
|
1459
1515
|
}
|
|
1460
1516
|
// 0. Replay-stats verdict (3.0.12). The native open now RECREATES a
|
|
1461
1517
|
// missing live log and recovers what its archives hold, so the
|
|
@@ -1487,6 +1543,7 @@ export class MetadataIndexManager {
|
|
|
1487
1543
|
catch {
|
|
1488
1544
|
// Verdict probe is advisory — a stats read failure must not block open.
|
|
1489
1545
|
}
|
|
1546
|
+
stage('replay-verdict');
|
|
1490
1547
|
// 1. Archive-orphan census over the memtable shard logs.
|
|
1491
1548
|
try {
|
|
1492
1549
|
const memtableDir = join(this.lsmMetaDir, 'memtable');
|
|
@@ -1513,11 +1570,13 @@ export class MetadataIndexManager {
|
|
|
1513
1570
|
catch {
|
|
1514
1571
|
// Census is advisory — an unreadable dir must not block open.
|
|
1515
1572
|
}
|
|
1573
|
+
stage('archive-census');
|
|
1516
1574
|
// 2. |canonical| == |posted| invariant.
|
|
1517
1575
|
try {
|
|
1518
1576
|
const getNouns = this.storage.getNouns;
|
|
1519
1577
|
if (typeof getNouns === 'function') {
|
|
1520
1578
|
const probe = await getNouns.call(this.storage, { pagination: { limit: 1 } });
|
|
1579
|
+
stage('canonical-probe');
|
|
1521
1580
|
const canonical = probe?.totalCount;
|
|
1522
1581
|
if (typeof canonical === 'number' &&
|
|
1523
1582
|
Number.isFinite(canonical) &&
|
|
@@ -1536,6 +1595,7 @@ export class MetadataIndexManager {
|
|
|
1536
1595
|
for (const t of Object.values(NounType)) {
|
|
1537
1596
|
posted += this.native.getEntityCountByType(t);
|
|
1538
1597
|
}
|
|
1598
|
+
stage('posted-count');
|
|
1539
1599
|
// LOWER-BOUND invariant, not equality: the adapter's totalCount
|
|
1540
1600
|
// EXCLUDES system-routed entities (e.g. the VFS root) while the
|
|
1541
1601
|
// index posts them, so healthy brains sit at posted ≥ canonical.
|
|
@@ -1555,6 +1615,7 @@ export class MetadataIndexManager {
|
|
|
1555
1615
|
// count-only and the allowance carries a bounded, logged
|
|
1556
1616
|
// staleness caveat.
|
|
1557
1617
|
const unpostable = await this.verifiedUnpostableAllowance();
|
|
1618
|
+
stage('unpostable-allowance');
|
|
1558
1619
|
if (posted + unpostable < canonical) {
|
|
1559
1620
|
this.strandDetected = true;
|
|
1560
1621
|
prodLog.error(`[NativeMetadataIndex] STRAND: canonical holds ${canonical} ` +
|
|
@@ -1575,6 +1636,7 @@ export class MetadataIndexManager {
|
|
|
1575
1636
|
catch {
|
|
1576
1637
|
// Invariant probe is advisory — a failed canonical read must not block open.
|
|
1577
1638
|
}
|
|
1639
|
+
stage('count-invariant');
|
|
1578
1640
|
}
|
|
1579
1641
|
// ==========================================================================
|
|
1580
1642
|
// Storage I/O helpers
|
|
@@ -1837,7 +1899,11 @@ export class MetadataIndexManager {
|
|
|
1837
1899
|
// Cache warming
|
|
1838
1900
|
// ==========================================================================
|
|
1839
1901
|
async warmCache() {
|
|
1902
|
+
// Elapsed-time narration: each warm stage is a loop of per-field
|
|
1903
|
+
// storage reads — stamp it when it exceeds 1s so a slow warm names
|
|
1904
|
+
// itself instead of hiding inside init's 'warm-cache' phase total.
|
|
1840
1905
|
const commonFields = ['noun', 'type', 'service', 'createdAt'];
|
|
1906
|
+
const t0 = Date.now();
|
|
1841
1907
|
await Promise.all(commonFields.map(async (field) => {
|
|
1842
1908
|
try {
|
|
1843
1909
|
await this.ensureFieldLoaded(field);
|
|
@@ -1846,7 +1912,15 @@ export class MetadataIndexManager {
|
|
|
1846
1912
|
prodLog.debug(`Cache warming: field '${field}' not yet indexed`);
|
|
1847
1913
|
}
|
|
1848
1914
|
}));
|
|
1915
|
+
if (Date.now() - t0 >= 1000) {
|
|
1916
|
+
opsLine(`warm-cache: common-field load (${commonFields.length} fields) took ` +
|
|
1917
|
+
`${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
1918
|
+
}
|
|
1919
|
+
const t1 = Date.now();
|
|
1849
1920
|
await this.warmCacheForTopTypes(3);
|
|
1921
|
+
if (Date.now() - t1 >= 1000) {
|
|
1922
|
+
opsLine(`warm-cache: top-type field load took ${((Date.now() - t1) / 1000).toFixed(1)}s`);
|
|
1923
|
+
}
|
|
1850
1924
|
}
|
|
1851
1925
|
async warmCacheForTopTypes(topN = 3) {
|
|
1852
1926
|
const topTypes = this.getTopNounTypes(topN);
|
|
@@ -1881,12 +1955,17 @@ export class MetadataIndexManager {
|
|
|
1881
1955
|
this.entityCountsByTypeFixed.fill(0);
|
|
1882
1956
|
this.verbCountsByTypeFixed.fill(0);
|
|
1883
1957
|
// Ensure 'noun' field is loaded into Rust
|
|
1958
|
+
const t0 = Date.now();
|
|
1884
1959
|
await this.ensureFieldLoaded('noun');
|
|
1960
|
+
if (Date.now() - t0 >= 1000) {
|
|
1961
|
+
opsLine(`lazy-counts: 'noun' field load took ${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
1962
|
+
}
|
|
1885
1963
|
// Use Rust to get type counts via the noun field. Count natively
|
|
1886
1964
|
// (#78 pt2) — getIdsCount returns the mapped-id count without
|
|
1887
1965
|
// materializing one UUID string per entity. At 10B this is the
|
|
1888
1966
|
// difference between O(1) per type and allocating billions of strings
|
|
1889
1967
|
// just to read `.length`.
|
|
1968
|
+
const t1 = Date.now();
|
|
1890
1969
|
const nounValues = this.native.getFilterValues('noun');
|
|
1891
1970
|
for (const typeName of nounValues) {
|
|
1892
1971
|
const count = this.native.getIdsCount('noun', JSON.stringify(typeName));
|
|
@@ -1894,6 +1973,10 @@ export class MetadataIndexManager {
|
|
|
1894
1973
|
this.totalEntitiesByType.set(typeName, count);
|
|
1895
1974
|
}
|
|
1896
1975
|
}
|
|
1976
|
+
if (Date.now() - t1 >= 1000) {
|
|
1977
|
+
opsLine(`lazy-counts: type-count scan over ${nounValues.length} noun value(s) took ` +
|
|
1978
|
+
`${((Date.now() - t1) / 1000).toFixed(1)}s`);
|
|
1979
|
+
}
|
|
1897
1980
|
prodLog.debug(`Loaded type counts: ${this.totalEntitiesByType.size} types`);
|
|
1898
1981
|
}
|
|
1899
1982
|
catch (error) {
|
|
@@ -2791,10 +2874,82 @@ export class MetadataIndexManager {
|
|
|
2791
2874
|
return false;
|
|
2792
2875
|
let upserts = 0;
|
|
2793
2876
|
let removals = 0;
|
|
2877
|
+
let batches = 0;
|
|
2878
|
+
let lastNarrated = 0;
|
|
2879
|
+
const replayStarted = Date.now();
|
|
2880
|
+
// Entry marker + first-batch liveness budget. The replay is ADVISORY
|
|
2881
|
+
// (false = the walk heals, always authoritative), so the scan's FIRST
|
|
2882
|
+
// batch gets a hard arrival budget: on a generation-backlogged brain
|
|
2883
|
+
// the storage-side scan wedged indefinitely before its first batch,
|
|
2884
|
+
// and the every-200 line below never fired — init froze silently
|
|
2885
|
+
// inside this await (2026-07-19). An optional answer must never be a
|
|
2886
|
+
// mandatory wait.
|
|
2887
|
+
// Proportionality: the routine catch-up (a boot replaying its own
|
|
2888
|
+
// last write, gap of a few generations, milliseconds) stays SILENT —
|
|
2889
|
+
// the suite's boots-2+-are-clean contract treats new boot noise as
|
|
2890
|
+
// heal activity, and it is right to. The entry line is deferred to
|
|
2891
|
+
// the first liveness event ≥1s; the budget abort always speaks.
|
|
2892
|
+
const FIRST_BATCH_BUDGET_MS = 120_000;
|
|
2893
|
+
// TOTAL wall-clock budget: the replay is an OPTIMIZATION over the
|
|
2894
|
+
// census walk, never a requirement — the walk stays authoritative
|
|
2895
|
+
// and heals in minutes. The batch-arrival budget alone missed the
|
|
2896
|
+
// second failure shape (2026-07-19): the scan produced batches fine
|
|
2897
|
+
// but the per-op index work ground at ~7s/record on a mint-storm-
|
|
2898
|
+
// scarred mapper — 321 upserts in 39 minutes against a 7,531-
|
|
2899
|
+
// generation gap, a replay that could NEVER beat the walk it was
|
|
2900
|
+
// optimizing away. Checked between ops: past the budget, abort
|
|
2901
|
+
// loudly to the walk.
|
|
2902
|
+
const TOTAL_REPLAY_BUDGET_MS = 300_000;
|
|
2903
|
+
const iterator = scan.batches()[Symbol.asyncIterator]();
|
|
2794
2904
|
try {
|
|
2795
|
-
for
|
|
2905
|
+
for (;;) {
|
|
2906
|
+
let timer;
|
|
2907
|
+
const budget = new Promise((resolve) => {
|
|
2908
|
+
timer = setTimeout(() => resolve('BUDGET'), FIRST_BATCH_BUDGET_MS);
|
|
2909
|
+
timer.unref?.();
|
|
2910
|
+
});
|
|
2911
|
+
const next = await Promise.race([iterator.next(), budget]);
|
|
2912
|
+
if (timer !== undefined)
|
|
2913
|
+
clearTimeout(timer);
|
|
2914
|
+
if (next === 'BUDGET') {
|
|
2915
|
+
opsLine(`fact replay from generation ${fromGeneration} ABORTED — the scan ` +
|
|
2916
|
+
`produced no batch within ${FIRST_BATCH_BUDGET_MS / 1000}s ` +
|
|
2917
|
+
`(${batches} batches arrived before the stall); the storage ` +
|
|
2918
|
+
`fact-scan path is unhealthy on this brain — falling back to ` +
|
|
2919
|
+
`the walk-based heal, which is always authoritative`);
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
if (next.done)
|
|
2923
|
+
break;
|
|
2924
|
+
const batch = next.value;
|
|
2925
|
+
// Liveness narration (loop-termination doctrine): a long replay
|
|
2926
|
+
// must narrate. If the loop wedges mid-batch, the LAST line's
|
|
2927
|
+
// counts localize the wedge; if scan.batches() itself stops
|
|
2928
|
+
// producing, this line's ABSENCE (after the caller's stage stamp)
|
|
2929
|
+
// localizes the producer instead.
|
|
2930
|
+
batches++;
|
|
2931
|
+
{
|
|
2932
|
+
// At most one liveness line per second, and none at all for a
|
|
2933
|
+
// sub-second replay — loud in proportion to the work.
|
|
2934
|
+
const now = Date.now();
|
|
2935
|
+
if (now - replayStarted >= 1000 && now - lastNarrated >= 1000) {
|
|
2936
|
+
lastNarrated = now;
|
|
2937
|
+
opsLine(`fact replay: ${batches} batch(es) from generation ${fromGeneration}, ` +
|
|
2938
|
+
`${upserts} upserts + ${removals} removals so far, ` +
|
|
2939
|
+
`${((now - replayStarted) / 1000).toFixed(1)}s elapsed`);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2796
2942
|
for (const fact of batch.facts) {
|
|
2797
2943
|
for (const op of fact.ops) {
|
|
2944
|
+
if (Date.now() - replayStarted > TOTAL_REPLAY_BUDGET_MS) {
|
|
2945
|
+
opsLine(`fact replay from generation ${fromGeneration} ABORTED — ` +
|
|
2946
|
+
`${upserts} upserts + ${removals} removals consumed the ` +
|
|
2947
|
+
`${TOTAL_REPLAY_BUDGET_MS / 1000}s total budget with the gap ` +
|
|
2948
|
+
`unfinished; this replay cannot beat the walk it optimizes ` +
|
|
2949
|
+
`away — falling back to the walk-based heal, which is ` +
|
|
2950
|
+
`always authoritative`);
|
|
2951
|
+
return false;
|
|
2952
|
+
}
|
|
2798
2953
|
if (op.record === null) {
|
|
2799
2954
|
await this.removeFromIndex(op.id);
|
|
2800
2955
|
removals++;
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
* means the stamp carries no sourceGeneration and verifiers skip the
|
|
11
11
|
* comparison — never a blocked write, never a guessed value.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Hard ceiling on the manifest-read fallback. The watermark is ADVISORY
|
|
15
|
+
* by contract (`undefined` = stamp carries no sourceGeneration, verifier
|
|
16
|
+
* skips) — so a storage layer that cannot answer promptly must cost a
|
|
17
|
+
* bounded wait and a loud line, never a frozen open (2026-07-19: a real
|
|
18
|
+
* brain state wedged this read indefinitely and init froze inside it,
|
|
19
|
+
* silent, until an external kill).
|
|
20
|
+
*/
|
|
21
|
+
const MANIFEST_READ_BUDGET_MS = 30_000;
|
|
13
22
|
/** Best-effort committed watermark; `undefined` when unknowable. */
|
|
14
23
|
export async function readCommittedGeneration(storage) {
|
|
15
24
|
try {
|
|
@@ -24,7 +33,22 @@ export async function readCommittedGeneration(storage) {
|
|
|
24
33
|
}
|
|
25
34
|
if (typeof s?.readRawObject !== 'function')
|
|
26
35
|
return undefined;
|
|
27
|
-
|
|
36
|
+
let timer;
|
|
37
|
+
const budget = new Promise((resolve) => {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
console.error(`[cor] watermark: manifest read exceeded its ${MANIFEST_READ_BUDGET_MS / 1000}s ` +
|
|
40
|
+
`budget — proceeding WITHOUT a sourceGeneration (advisory contract); ` +
|
|
41
|
+
`the storage layer's read path is unhealthy and should be investigated`);
|
|
42
|
+
resolve(undefined);
|
|
43
|
+
}, MANIFEST_READ_BUDGET_MS);
|
|
44
|
+
timer.unref?.();
|
|
45
|
+
});
|
|
46
|
+
const manifest = (await Promise.race([
|
|
47
|
+
s.readRawObject('_system/manifest.json'),
|
|
48
|
+
budget,
|
|
49
|
+
]));
|
|
50
|
+
if (timer !== undefined)
|
|
51
|
+
clearTimeout(timer);
|
|
28
52
|
const g = manifest?.generation;
|
|
29
53
|
return typeof g === 'number' && Number.isFinite(g) && g >= 0 ? g : undefined;
|
|
30
54
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
* `check:brainy`-style drift between the two fails the release.
|
|
12
12
|
*/
|
|
13
13
|
/** The @soulcraft/cor version this build was cut from. */
|
|
14
|
-
export declare const COR_VERSION = "3.0.
|
|
14
|
+
export declare const COR_VERSION = "3.0.22";
|
|
15
15
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
Binary file
|
package/native/index.d.ts
CHANGED
|
@@ -121,8 +121,10 @@ export declare class NativeBinaryIdMapper {
|
|
|
121
121
|
*/
|
|
122
122
|
getOrAssign(uuid: Buffer): number
|
|
123
123
|
/**
|
|
124
|
-
* Lookup the int for `uuid`. Returns null
|
|
125
|
-
*
|
|
124
|
+
* Lookup the int for `uuid`. Returns null only when the mapping
|
|
125
|
+
* is PROVEN absent; an unreadable store throws instead of
|
|
126
|
+
* masquerading as null. Throws in `IdSpace::U64` mode — use
|
|
127
|
+
* [`get_int_big`].
|
|
126
128
|
*/
|
|
127
129
|
getInt(uuid: Buffer): number | null
|
|
128
130
|
/**
|
|
@@ -156,7 +158,10 @@ export declare class NativeBinaryIdMapper {
|
|
|
156
158
|
* a JS number).
|
|
157
159
|
*/
|
|
158
160
|
getOrAssignBig(uuid: Buffer): bigint
|
|
159
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* BigInt sibling of [`get_int`]. Same null contract: null =
|
|
163
|
+
* proven absent, unreadable store = throw.
|
|
164
|
+
*/
|
|
160
165
|
getIntBig(uuid: Buffer): bigint | null
|
|
161
166
|
/** BigInt sibling of [`get_uuid`]. */
|
|
162
167
|
getUuidBig(int: bigint): Buffer | null
|
|
@@ -1796,7 +1801,8 @@ export declare class NativeMetadataIndex {
|
|
|
1796
1801
|
uuidToInt(uuid: string): number | null
|
|
1797
1802
|
/**
|
|
1798
1803
|
* BigInt sibling of [`uuid_to_int`]. Works for both U32 and U64
|
|
1799
|
-
* brains; preferred by brainy 8.0 consumers.
|
|
1804
|
+
* brains; preferred by brainy 8.0 consumers. Null means PROVEN
|
|
1805
|
+
* unmapped; an unreadable mapper store throws.
|
|
1800
1806
|
*/
|
|
1801
1807
|
uuidToIntBig(uuid: string): bigint | null
|
|
1802
1808
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/cor",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.22",
|
|
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.
|
|
85
|
+
"@soulcraft/brainy": "8.7.0",
|
|
86
86
|
"@types/node": "^22.0.0",
|
|
87
87
|
"pg": "^8.21.0",
|
|
88
88
|
"tsx": "^4.21.0",
|