@soulcraft/cor 3.0.0-rc.1

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.
Files changed (87) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +328 -0
  3. package/dist/aggregation/NativeAggregationEngine.d.ts +118 -0
  4. package/dist/aggregation/NativeAggregationEngine.js +186 -0
  5. package/dist/cli.d.ts +12 -0
  6. package/dist/cli.js +236 -0
  7. package/dist/graph/GraphAccelerationAdapter.d.ts +153 -0
  8. package/dist/graph/GraphAccelerationAdapter.js +326 -0
  9. package/dist/graph/NativeGraphAdjacencyIndex.d.ts +286 -0
  10. package/dist/graph/NativeGraphAdjacencyIndex.js +944 -0
  11. package/dist/hnsw/AdaptiveDiskAnnModeSelector.d.ts +187 -0
  12. package/dist/hnsw/AdaptiveDiskAnnModeSelector.js +313 -0
  13. package/dist/hnsw/NativeDiskAnnWrapper.d.ts +489 -0
  14. package/dist/hnsw/NativeDiskAnnWrapper.js +1456 -0
  15. package/dist/index.d.ts +28 -0
  16. package/dist/index.js +29 -0
  17. package/dist/legacyLayoutGuard.d.ts +93 -0
  18. package/dist/legacyLayoutGuard.js +237 -0
  19. package/dist/license/constants.d.ts +30 -0
  20. package/dist/license/constants.js +30 -0
  21. package/dist/license/onlineValidator.d.ts +43 -0
  22. package/dist/license/onlineValidator.js +225 -0
  23. package/dist/license/types.d.ts +39 -0
  24. package/dist/license/types.js +5 -0
  25. package/dist/license.d.ts +64 -0
  26. package/dist/license.js +165 -0
  27. package/dist/migration/MigrationCoordinator.d.ts +171 -0
  28. package/dist/migration/MigrationCoordinator.js +249 -0
  29. package/dist/migration/indexEpochGuard.d.ts +163 -0
  30. package/dist/migration/indexEpochGuard.js +202 -0
  31. package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
  32. package/dist/native/NativeEmbeddingEngine.js +318 -0
  33. package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
  34. package/dist/native/NativeRoaringBitmap32.js +221 -0
  35. package/dist/native/ffi.d.ts +20 -0
  36. package/dist/native/ffi.js +48 -0
  37. package/dist/native/idMapperTestSupport.d.ts +67 -0
  38. package/dist/native/idMapperTestSupport.js +112 -0
  39. package/dist/native/index.d.ts +49 -0
  40. package/dist/native/index.js +87 -0
  41. package/dist/native/napi.d.ts +21 -0
  42. package/dist/native/napi.js +88 -0
  43. package/dist/native/types.d.ts +1298 -0
  44. package/dist/native/types.js +16 -0
  45. package/dist/plugin.d.ts +50 -0
  46. package/dist/plugin.js +388 -0
  47. package/dist/providerContracts.d.ts +277 -0
  48. package/dist/providerContracts.js +38 -0
  49. package/dist/resource/OsMemoryProbe.d.ts +175 -0
  50. package/dist/resource/OsMemoryProbe.js +206 -0
  51. package/dist/resource/ResourceManager.d.ts +491 -0
  52. package/dist/resource/ResourceManager.js +960 -0
  53. package/dist/utils/ColumnManifest.d.ts +97 -0
  54. package/dist/utils/ColumnManifest.js +129 -0
  55. package/dist/utils/NativeColumnStore.d.ts +284 -0
  56. package/dist/utils/NativeColumnStore.js +685 -0
  57. package/dist/utils/NativeMetadataIndex.d.ts +882 -0
  58. package/dist/utils/NativeMetadataIndex.js +2631 -0
  59. package/dist/utils/NativeUnifiedCache.d.ts +87 -0
  60. package/dist/utils/NativeUnifiedCache.js +273 -0
  61. package/dist/utils/binaryIdMapperFactory.d.ts +59 -0
  62. package/dist/utils/binaryIdMapperFactory.js +94 -0
  63. package/dist/utils/collation.d.ts +30 -0
  64. package/dist/utils/collation.js +40 -0
  65. package/dist/utils/columnStoreTypes.d.ts +161 -0
  66. package/dist/utils/columnStoreTypes.js +29 -0
  67. package/dist/utils/nativeBinaryEntityIdMapper.d.ts +211 -0
  68. package/dist/utils/nativeBinaryEntityIdMapper.js +381 -0
  69. package/dist/utils/nativeEntityIdMapper.d.ts +111 -0
  70. package/dist/utils/nativeEntityIdMapper.js +170 -0
  71. package/docs/ADR-002-diskann-100-percent-rust.md +294 -0
  72. package/docs/ADR-003-semantic-time-travel.md +100 -0
  73. package/docs/aggregation.md +251 -0
  74. package/docs/comparison.md +162 -0
  75. package/docs/deployment-limits.md +87 -0
  76. package/docs/diskann.md +184 -0
  77. package/docs/migration-3.0.md +396 -0
  78. package/docs/performance-budget.md +274 -0
  79. package/docs/performance.md +117 -0
  80. package/docs/scaling.md +439 -0
  81. package/docs/snapshot-safety.md +246 -0
  82. package/docs/u64-id-space.md +214 -0
  83. package/docs/verification-report.md +670 -0
  84. package/native/brainy-native.node +0 -0
  85. package/native/index.d.ts +3916 -0
  86. package/package.json +92 -0
  87. package/scripts/migrate-cortex-2x-to-3x.mjs +970 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @module resource/OsMemoryProbe
3
+ * @description OS-level memory observer for the cor 3.0 **Adaptive
4
+ * DiskANN** mode selector (Piece J of the cor 3.0 plan).
5
+ *
6
+ * The {@link ResourceManager} already observes the V8 heap and a
7
+ * coarse system-RSS pressure ratio (Piece 8). What it didn't have
8
+ * was a read on the kernel's `MemAvailable` figure — the metric the
9
+ * Linux kernel computes as "how much physical RAM can a new
10
+ * allocation claim without swapping," factoring in reclaimable page
11
+ * cache and slab. That's the right signal for the adaptive selector
12
+ * to pick a DiskANN open mode in multi-tenant scenarios where a
13
+ * shared box runs many brainy+cor instances: each instance's
14
+ * `os.freemem()` undercounts available memory (it reports MemFree,
15
+ * not MemAvailable), so a naïve `freemem`-based check would push
16
+ * the selector toward OnDisk hints even when there's plenty of
17
+ * reclaimable cache available.
18
+ *
19
+ * ## Surface
20
+ *
21
+ * - {@link parseMeminfo} — pure parser; fixture-friendly.
22
+ * - {@link OsMemoryProbe} — readers + caching wrapper. Dependency-
23
+ * injected so tests can simulate Linux on macOS and exercise the
24
+ * `MemAvailable` missing branch without touching the real
25
+ * `/proc/meminfo`.
26
+ *
27
+ * ## Why a cache
28
+ *
29
+ * `/proc/meminfo` reads cost ~10 μs of syscall time — not free, but
30
+ * also not worth re-reading on every single call. The 250 ms cache
31
+ * is comfortably below the 5 s pressure-tick cadence and the 30 s
32
+ * rebalance cadence the ResourceManager already runs, so any
33
+ * meaningful pressure event lands on a fresh read.
34
+ *
35
+ * ## Non-Linux fallback
36
+ *
37
+ * On macOS and Windows there is no `/proc/meminfo`. The probe falls
38
+ * back to `os.totalmem()` + `os.freemem()` and marks the snapshot
39
+ * `source: 'os-fallback'` so callers can degrade gracefully (the
40
+ * adaptive selector currently treats fallback snapshots as
41
+ * "available bytes is conservative, prefer Hybrid over OnDisk").
42
+ */
43
+ import os from 'node:os';
44
+ import { readFileSync } from 'node:fs';
45
+ const KIB = 1024;
46
+ /**
47
+ * Match a `Field: value [kB]` line in `/proc/meminfo`.
48
+ *
49
+ * Allowed key chars cover the punctuation that real kernel-side
50
+ * names use: `Active(anon)`, `Inactive(file)`, etc. We don't read
51
+ * those fields ourselves, but the regex tolerates them so a typo in
52
+ * a future key extension doesn't silently fall through.
53
+ */
54
+ const FIELD_PATTERN = /^([A-Za-z()_]+):\s+(\d+)(?:\s+kB)?\s*$/;
55
+ /**
56
+ * Parse a `/proc/meminfo` text blob and extract the three fields the
57
+ * probe needs. Pure function — no I/O, fully deterministic, the
58
+ * only thing fixture tests need to cover.
59
+ *
60
+ * Returns `null` only when `MemTotal` is missing, which would mean
61
+ * the input isn't `/proc/meminfo` at all (every Linux kernel since
62
+ * the file existed has emitted it). Missing `MemAvailable` or
63
+ * `MemFree` is normal on old or specialised kernels and is left for
64
+ * the snapshot composer to handle.
65
+ */
66
+ export function parseMeminfo(text) {
67
+ const fields = {};
68
+ for (const rawLine of text.split('\n')) {
69
+ const line = rawLine.trimEnd();
70
+ if (line.length === 0)
71
+ continue;
72
+ const match = line.match(FIELD_PATTERN);
73
+ if (!match)
74
+ continue;
75
+ const key = match[1];
76
+ const value = parseInt(match[2], 10);
77
+ if (!Number.isFinite(value) || value < 0)
78
+ continue;
79
+ if (key === 'MemTotal')
80
+ fields.totalKib = value;
81
+ else if (key === 'MemAvailable')
82
+ fields.availableKib = value;
83
+ else if (key === 'MemFree')
84
+ fields.freeKib = value;
85
+ }
86
+ if (fields.totalKib === undefined)
87
+ return null;
88
+ return fields;
89
+ }
90
+ function clamp01(x) {
91
+ if (!Number.isFinite(x))
92
+ return 0;
93
+ if (x < 0)
94
+ return 0;
95
+ if (x > 1)
96
+ return 1;
97
+ return x;
98
+ }
99
+ /**
100
+ * Build an {@link OsMemorySnapshot} from the parsed `/proc/meminfo`
101
+ * fields. Visible for tests; production callers use
102
+ * {@link OsMemoryProbe.snapshot}.
103
+ */
104
+ export function snapshotFromMeminfo(fields) {
105
+ const totalBytes = (fields.totalKib ?? 0) * KIB;
106
+ const freeBytes = (fields.freeKib ?? 0) * KIB;
107
+ // MemAvailable landed in Linux 3.14. Older kernels need the
108
+ // MemFree fallback — strictly worse signal but the best we have.
109
+ const availableBytes = fields.availableKib !== undefined ? fields.availableKib * KIB : freeBytes;
110
+ const pressureScore = totalBytes > 0 ? clamp01(1 - availableBytes / totalBytes) : 0;
111
+ return {
112
+ totalBytes,
113
+ availableBytes,
114
+ freeBytes,
115
+ pressureScore,
116
+ source: 'proc-meminfo',
117
+ };
118
+ }
119
+ /**
120
+ * Build the os-fallback snapshot from Node's `os` module. Visible
121
+ * for tests; production callers use {@link OsMemoryProbe.snapshot}.
122
+ */
123
+ export function snapshotFromOs() {
124
+ const totalBytes = os.totalmem();
125
+ const freeBytes = os.freemem();
126
+ // No MemAvailable equivalent without a platform-specific syscall;
127
+ // freemem is the best approximation available cross-platform.
128
+ const pressureScore = totalBytes > 0 ? clamp01(1 - freeBytes / totalBytes) : 0;
129
+ return {
130
+ totalBytes,
131
+ availableBytes: freeBytes,
132
+ freeBytes,
133
+ pressureScore,
134
+ source: 'os-fallback',
135
+ };
136
+ }
137
+ /**
138
+ * Default `/proc/meminfo` reader. Returns the file's text or `null`
139
+ * when the file doesn't exist / is unreadable (the expected case on
140
+ * macOS and Windows). Synchronous because the file is virtual and
141
+ * reads cost microseconds.
142
+ */
143
+ function defaultProcReader() {
144
+ try {
145
+ return readFileSync('/proc/meminfo', 'utf8');
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * Cached `/proc/meminfo` observer. Wire one instance into the
153
+ * {@link ResourceManager}; the adaptive DiskANN selector (Piece I)
154
+ * will read `snapshot()` at brain-open time.
155
+ */
156
+ export class OsMemoryProbe {
157
+ cacheMs;
158
+ procReader;
159
+ osFallback;
160
+ now;
161
+ cached = null;
162
+ constructor(options = {}) {
163
+ this.cacheMs = options.cacheMs ?? 250;
164
+ this.procReader = options.procReader ?? defaultProcReader;
165
+ this.osFallback = options.osFallback ?? snapshotFromOs;
166
+ this.now = options.now ?? Date.now;
167
+ }
168
+ /**
169
+ * Latest memory snapshot. Cached for {@link OsMemoryProbeOptions.cacheMs}
170
+ * milliseconds; calls inside the window reuse the cached value.
171
+ */
172
+ snapshot() {
173
+ const now = this.now();
174
+ if (this.cached && this.cached.expires > now) {
175
+ return this.cached.snapshot;
176
+ }
177
+ const text = this.procReader();
178
+ let fresh;
179
+ if (text !== null) {
180
+ const fields = parseMeminfo(text);
181
+ if (fields !== null) {
182
+ fresh = snapshotFromMeminfo(fields);
183
+ }
184
+ else {
185
+ // The proc reader succeeded but the parse failed — malformed
186
+ // file. Degrade rather than throw so a flaky kernel doesn't
187
+ // crash the whole observation surface.
188
+ fresh = this.osFallback();
189
+ }
190
+ }
191
+ else {
192
+ fresh = this.osFallback();
193
+ }
194
+ this.cached = { snapshot: fresh, expires: now + this.cacheMs };
195
+ return fresh;
196
+ }
197
+ /**
198
+ * Invalidate the cache so the next {@link snapshot} call performs
199
+ * a fresh read. Used by tests; production callers shouldn't need
200
+ * this because the cache TTL is short.
201
+ */
202
+ invalidate() {
203
+ this.cached = null;
204
+ }
205
+ }
206
+ //# sourceMappingURL=OsMemoryProbe.js.map
@@ -0,0 +1,491 @@
1
+ /**
2
+ * @module resource/ResourceManager
3
+ * @description Adaptive resource manager for multi-instance Brainy deployments.
4
+ *
5
+ * Observes system resources (RAM, CPU, cgroups limits, active instance count)
6
+ * and dynamically adjusts cache sizing and memory budgets. All products
7
+ * (Workshop, Venue, Academy) benefit automatically — zero configuration needed.
8
+ *
9
+ * Singleton — one ResourceManager per Node.js process, shared across all
10
+ * Brainy instances. Registered as a Cor provider so SDK and brainy can
11
+ * query resource profiles without importing cor directly.
12
+ *
13
+ * ## Per-category memory budget (Piece 8 extension, cor 3.0)
14
+ *
15
+ * The cache budget is split across four named categories — `vectors`,
16
+ * `metadata`, `embedding`, `other` — each with a hardcoded base share of
17
+ * the total cache pool (50% / 25% / 15% / 10% respectively). Subsystems
18
+ * (cor's native LSM metadata index, DiskANN graph, embedding cache, etc.)
19
+ * register as `MemoryBudgetSubscriber`s with their category and a callback
20
+ * that fires whenever their effective allocation changes.
21
+ *
22
+ * Per the zero-config principle: the base shares are the **starting point**,
23
+ * not a hard cap. The ResourceManager observes actual utilization
24
+ * (`reportNativeMemory(name, bytes)` calls) on a 5-second pressure-tick
25
+ * cadence and **dynamically rebalances** allocations at runtime —
26
+ * under-utilized categories donate capacity to over-utilized ones,
27
+ * bounded so no category can shrink below 25% of its base share or grow
28
+ * above 200% of its base share. Operators never tune the split; the
29
+ * system adapts to its workload.
30
+ *
31
+ * The pre-existing `setCacheResizeCallback` API is preserved as a
32
+ * back-compat shim that auto-subscribes to the `other` category with a
33
+ * zero-bytes report — no behavior change for existing wiring.
34
+ *
35
+ * ## OS-memory observation (Piece J, cor 3.0)
36
+ *
37
+ * A built-in {@link OsMemoryProbe} reads `/proc/meminfo` on Linux
38
+ * (with an `os.totalmem`/`os.freemem` fallback elsewhere) and
39
+ * exposes the result via {@link getResourceProfile}'s `osMemory`
40
+ * field and the convenience accessor
41
+ * {@link ResourceManager.getOsMemorySnapshot}. The **Adaptive
42
+ * DiskANN** mode selector (Piece I) reads this snapshot at
43
+ * brain-open time to pick between in-memory / hybrid / on-disk
44
+ * operating modes — the key signal is `availableBytes`
45
+ * (`MemAvailable` on Linux), which counts reclaimable page cache
46
+ * as "available for new allocations." Naïve `os.freemem()`
47
+ * undercounts in multi-tenant scenarios where many brainy+cor
48
+ * instances share a box.
49
+ */
50
+ import { OsMemoryProbe, type OsMemorySnapshot } from './OsMemoryProbe.js';
51
+ /**
52
+ * Snapshot of system resources and computed recommendations.
53
+ */
54
+ export interface ResourceProfile {
55
+ /** Total system memory in bytes */
56
+ totalMemory: number;
57
+ /** Free system memory in bytes (at observation time) */
58
+ freeMemory: number;
59
+ /** Effective memory limit (cgroups-aware: min of system and cgroup limit) */
60
+ effectiveLimit: number;
61
+ /** Whether running in a container (cgroups detected) */
62
+ isContainer: boolean;
63
+ /** Process RSS in bytes */
64
+ processRss: number;
65
+ /** Process heap used in bytes */
66
+ heapUsed: number;
67
+ /** Number of CPU cores */
68
+ cpuCount: number;
69
+ /** Number of active brainy instances tracked */
70
+ activeInstances: number;
71
+ /** Total entities across all active instances */
72
+ totalEntities: number;
73
+ /** Recommended cache budget in bytes */
74
+ recommendedCacheSize: number;
75
+ /** Memory pressure level */
76
+ pressure: 'normal' | 'elevated' | 'critical';
77
+ /** Per-instance average memory estimate in bytes */
78
+ avgInstanceMemory: number;
79
+ /** Estimated headroom for new instances in bytes */
80
+ headroom: number;
81
+ /**
82
+ * **Piece 8 — V8 heap tracking.** Brainy's TS-side memory pressure
83
+ * is a separate signal from system RSS pressure: the V8 heap has its
84
+ * own hard ceiling (controlled by `--max-old-space-size`, default
85
+ * ~4 GB on Node) and GC starts thrashing well before the system is
86
+ * out of RAM. Tracked so subsystems (filter lowering, iter-spill,
87
+ * VFS pool) can shed JS allocations when the heap fills even if
88
+ * system memory is plentiful.
89
+ */
90
+ v8Heap: V8HeapSnapshot;
91
+ /**
92
+ * **Piece J — OS-memory observation.** Snapshot of the
93
+ * kernel-reported `MemTotal` / `MemAvailable` / `MemFree` (Linux)
94
+ * or `os.totalmem` / `os.freemem` (fallback). Distinct from
95
+ * {@link processRss} (which is the THIS process's footprint) and
96
+ * from {@link freeMemory} (which is the legacy `os.freemem()`
97
+ * reading). The Adaptive DiskANN mode selector (Piece I) reads
98
+ * `osMemory.availableBytes` to decide which open mode to apply.
99
+ */
100
+ osMemory: OsMemorySnapshot;
101
+ }
102
+ /**
103
+ * Snapshot of V8 heap state at observation time. Mirrors the subset
104
+ * of `v8.getHeapStatistics()` that matters for budget decisions, plus
105
+ * the derived pressure classification.
106
+ */
107
+ export interface V8HeapSnapshot {
108
+ /** `used_heap_size` — live JS object footprint */
109
+ usedHeapSize: number;
110
+ /** `heap_size_limit` — V8's hard ceiling before OOM */
111
+ heapSizeLimit: number;
112
+ /** `external_memory` — buffers + mmap'd regions outside the JS heap */
113
+ externalMemory: number;
114
+ /** `used_heap_size / heap_size_limit` (0..1) */
115
+ ratio: number;
116
+ /**
117
+ * Pressure classification — same three-level shape as the
118
+ * system-RSS `pressure` field for consistency. Thresholds chosen
119
+ * to fire BEFORE V8's GC starts thrashing:
120
+ * - `elevated` ≥ 0.75 ratio (matches system PRESSURE_ELEVATED)
121
+ * - `critical` ≥ 0.90 ratio (matches system PRESSURE_CRITICAL)
122
+ *
123
+ * Subsystems should treat `elevated` as "stop growing
124
+ * allocations, prefer iter-spill paths" and `critical` as "shed
125
+ * allocations now, before the next minor GC".
126
+ */
127
+ pressure: 'normal' | 'elevated' | 'critical';
128
+ }
129
+ export type { OsMemorySnapshot } from './OsMemoryProbe.js';
130
+ /**
131
+ * The named memory categories that share the cache budget. Subsystems
132
+ * register against one of these so the ResourceManager can rebalance
133
+ * allocations between them based on observed pressure.
134
+ */
135
+ export type MemoryCategory = 'vectors' | 'metadata' | 'embedding' | 'other';
136
+ /**
137
+ * Callback fired when a subscriber's effective allocation changes. The
138
+ * `allowedBytes` value is the **current** ceiling — subsystems should
139
+ * cap their resident bytes at this number; if they're currently over,
140
+ * they should start shedding (evict, flush, etc).
141
+ */
142
+ export type MemoryBudgetCallback = (allowedBytes: number) => void;
143
+ /**
144
+ * Adaptive resource manager for multi-instance Brainy deployments.
145
+ *
146
+ * Tracks active brainy instances, observes system resources, and computes
147
+ * dynamic memory budgets for cache sizing and pool eviction.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const rm = ResourceManager.getInstance()
152
+ * rm.trackInstance('tenant-wicks', 450)
153
+ *
154
+ * const profile = rm.getResourceProfile()
155
+ * console.log(`Cache budget: ${profile.recommendedCacheSize / 1024 / 1024}MB`)
156
+ * console.log(`Headroom for new instances: ${profile.headroom / 1024 / 1024}MB`)
157
+ * ```
158
+ */
159
+ export declare class ResourceManager {
160
+ private static instance;
161
+ private readonly instances;
162
+ private readonly effectiveLimit;
163
+ private readonly isContainer;
164
+ private rebalanceTimer;
165
+ private onCacheResize;
166
+ /**
167
+ * **Piece J — OS-memory observation.** `/proc/meminfo`-backed
168
+ * snapshot source. Held as an instance field so tests can swap
169
+ * the probe via {@link _testInjectOsMemoryProbe}.
170
+ */
171
+ private osMemoryProbe;
172
+ /**
173
+ * Active per-category memory subscribers, keyed by name. Each
174
+ * subscriber receives notifications when its effective allocation
175
+ * changes (whether from a whole-pool rebalance or a category-level
176
+ * dynamic adaptation).
177
+ */
178
+ private readonly subscribers;
179
+ /**
180
+ * Current dynamic share for each category. Starts at `BASE_CATEGORY_SHARES`
181
+ * and adapts at runtime to reported pressure. Sum across categories is
182
+ * preserved at exactly 1.0 by the rebalance algorithm.
183
+ */
184
+ private categoryShares;
185
+ /**
186
+ * 5-second pressure-tick timer. Only running while at least one
187
+ * category is at or above the high-water threshold; auto-stops once
188
+ * every category falls back below the low-water threshold.
189
+ */
190
+ private pressureTickTimer;
191
+ /**
192
+ * Tracks how many consecutive ticks have shown no pressure — used to
193
+ * decide when to stop the pressure-tick.
194
+ */
195
+ private quietTicks;
196
+ /** One-shot guard for the multi-tenant OS-limits deployment warning. */
197
+ private osLimitsWarned;
198
+ private constructor();
199
+ /**
200
+ * Get the singleton ResourceManager instance.
201
+ */
202
+ static getInstance(): ResourceManager;
203
+ /**
204
+ * **Back-compat callback** for the cortex 2.x cache resize wiring.
205
+ * The cor plugin (`src/plugin.ts`) registers brainy's
206
+ * `UnifiedCache.setMaxSize` via this method; the callback fires on
207
+ * every whole-pool rebalance with the TOTAL cache budget (not a
208
+ * per-category slice).
209
+ *
210
+ * Behavior is unchanged from cortex 2.x: the callback receives the
211
+ * full `recommendedCacheSize` from `getResourceProfile()` whenever
212
+ * the pool resizes. The new per-category subscriber API
213
+ * ([`registerMemoryBudgetSubscriber`](#registerMemoryBudgetSubscriber))
214
+ * runs in PARALLEL — they don't interfere. Legacy callers can keep
215
+ * their existing wiring; new code that wants a category-specific
216
+ * allocation registers as a subscriber.
217
+ *
218
+ * @param callback - Called with the new recommended cache size in bytes
219
+ */
220
+ setCacheResizeCallback(callback: (newSize: number) => void): void;
221
+ /**
222
+ * Register a subscriber for per-category memory budget notifications.
223
+ *
224
+ * Subscribers receive an `allowedBytes` value whenever their effective
225
+ * allocation changes — either because the overall pool resized (system
226
+ * memory shift, instance count change) or because the dynamic
227
+ * rebalance loop shifted capacity between categories under observed
228
+ * pressure.
229
+ *
230
+ * The returned function deregisters the subscriber. Each `name` must
231
+ * be unique; registering twice with the same name replaces the prior
232
+ * registration.
233
+ *
234
+ * @param name - Unique subscriber name (used with `reportNativeMemory`)
235
+ * @param category - The category this subscriber draws from
236
+ * @param callback - Invoked with the current `allowedBytes` ceiling
237
+ * @returns A deregistration function (call to detach the subscriber)
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * const rm = ResourceManager.getInstance()
242
+ * const unsubscribe = rm.registerMemoryBudgetSubscriber(
243
+ * 'metadata-lsm',
244
+ * 'metadata',
245
+ * (allowed) => { lsm.setMaxBytes(allowed) }
246
+ * )
247
+ * // Later, when the LSM measures its footprint:
248
+ * rm.reportNativeMemory('metadata-lsm', lsm.currentBytes())
249
+ * // To detach:
250
+ * unsubscribe()
251
+ * ```
252
+ */
253
+ registerMemoryBudgetSubscriber(name: string, category: MemoryCategory, callback: MemoryBudgetCallback): () => void;
254
+ /**
255
+ * Report the current resident-memory footprint of a registered
256
+ * subscriber, in bytes. The rebalance loop uses these values to
257
+ * detect category-level pressure and shift capacity dynamically.
258
+ *
259
+ * Call this whenever the subscriber's footprint changes meaningfully
260
+ * (after a flush, after a major allocation, etc). The pressure-tick
261
+ * fires within 5 seconds of any single category exceeding 90% of its
262
+ * current allocation; calling more often is harmless.
263
+ *
264
+ * Unknown names are silently dropped — a subscriber that has
265
+ * deregistered may still have pending reports in flight.
266
+ */
267
+ reportNativeMemory(name: string, bytes: number): void;
268
+ /**
269
+ * Report the current dynamic share for each category. Mostly used by
270
+ * tests + the diagnostics surface; production callers should subscribe
271
+ * via [`registerMemoryBudgetSubscriber`] instead of polling.
272
+ *
273
+ * The sum across categories is always exactly 1.0 (modulo float
274
+ * rounding); allocations sum to the cache budget.
275
+ */
276
+ getCategoryShares(): Readonly<Record<MemoryCategory, number>>;
277
+ /**
278
+ * The current per-category allowed-bytes ceiling. Same as what an
279
+ * active subscriber received in its last callback. Provided for
280
+ * diagnostics; production callers subscribe instead of polling.
281
+ */
282
+ getCategoryAllowedBytes(category: MemoryCategory): number;
283
+ /**
284
+ * Start periodic rebalancing (every 30s). Aligns with brainy's existing
285
+ * memory pressure monitoring interval.
286
+ */
287
+ startRebalancing(): void;
288
+ /**
289
+ * Stop periodic rebalancing.
290
+ */
291
+ stopRebalancing(): void;
292
+ /**
293
+ * Start the 5-second pressure-tick if it isn't already running.
294
+ * Idempotent. Called from `reportNativeMemory` when a category crosses
295
+ * the high-water threshold; stops on its own when every category
296
+ * falls back below the low-water threshold for 3 consecutive ticks.
297
+ */
298
+ private ensurePressureTickRunning;
299
+ private stopPressureTick;
300
+ /**
301
+ * Test-only hook to drive a pressure tick synchronously without
302
+ * waiting for the timer. Not exposed to production callers; tests
303
+ * use this to assert the rebalance behaviour deterministically.
304
+ * @internal
305
+ */
306
+ _testTickPressure(): void;
307
+ /**
308
+ * Track a new or updated brainy instance. Call when a brainy instance is
309
+ * created or when its entity count changes significantly.
310
+ *
311
+ * @param key - Unique scope key for this instance (e.g. tenant slug, user:workspace)
312
+ * @param entityCount - Current entity count in this instance
313
+ */
314
+ trackInstance(key: string, entityCount: number): void;
315
+ /**
316
+ * One-shot deployment warning: when enough brains are pooled in ONE
317
+ * process that OS file/map limits could bottleneck before RAM — and the
318
+ * limits are below the projected need — log the exact fix. Cor
319
+ * auto-raises the SOFT `nofile` limit itself; the hard cap +
320
+ * `vm.max_map_count` must be set by the deployment (a process cannot
321
+ * raise those). Context-aware: silent for a single big brain (Venue);
322
+ * fires only at real pool scale. See `docs/ADR-004-multi-tenant-storage.md`.
323
+ */
324
+ private maybeWarnOsLimits;
325
+ /**
326
+ * Stop tracking a brainy instance. Call when the instance is evicted or closed.
327
+ *
328
+ * @param key - The scope key that was passed to trackInstance()
329
+ */
330
+ untrackInstance(key: string): void;
331
+ /**
332
+ * Record an access to an instance (updates lastAccess for weighted eviction).
333
+ *
334
+ * @param key - The scope key
335
+ */
336
+ touchInstance(key: string): void;
337
+ /**
338
+ * **Piece 8 — V8 heap tracking.** Snapshot the current V8 heap
339
+ * state and classify pressure. Cheap call (a single
340
+ * `v8.getHeapStatistics()` syscall + a few divisions); fine to
341
+ * invoke per-query on the hot path. Subsystems like brainy's filter
342
+ * lowering use this to early-exit before allocating big
343
+ * intermediate bitmaps that would push the heap over the ceiling.
344
+ *
345
+ * The pressure classification uses the same three-level shape as
346
+ * system-RSS pressure (`normal` / `elevated` / `critical`) so
347
+ * subscribers can branch uniformly.
348
+ */
349
+ getHeapStats(): V8HeapSnapshot;
350
+ /**
351
+ * **Piece 8.** Convenience accessor returning just the heap
352
+ * pressure classification — cheaper than constructing a full
353
+ * {@link V8HeapSnapshot} when the caller only needs the branch.
354
+ */
355
+ getHeapPressure(): 'normal' | 'elevated' | 'critical';
356
+ /**
357
+ * **Piece J.** Latest OS-memory snapshot from `/proc/meminfo`
358
+ * (Linux) or the os-fallback path. Cached for 250 ms by the
359
+ * underlying {@link OsMemoryProbe}; consecutive calls inside the
360
+ * cache window reuse the value. Cheaper than
361
+ * {@link getResourceProfile} when the caller only needs the
362
+ * memory snapshot (the Adaptive DiskANN selector — Piece I — is
363
+ * the canonical caller).
364
+ */
365
+ getOsMemorySnapshot(): OsMemorySnapshot;
366
+ /**
367
+ * Test-only hook to swap the OS-memory probe with a deterministic
368
+ * stand-in. Production code must not call this — the singleton's
369
+ * probe is part of the resource-observation contract.
370
+ * @internal
371
+ */
372
+ _testInjectOsMemoryProbe(probe: OsMemoryProbe): void;
373
+ /**
374
+ * Compute a full resource profile snapshot.
375
+ *
376
+ * @returns Current system resources, instance stats, and recommendations
377
+ */
378
+ getResourceProfile(): ResourceProfile;
379
+ /**
380
+ * Check if there's enough memory headroom to create a new instance
381
+ * with the given entity count.
382
+ *
383
+ * @param estimatedEntities - Expected entity count for the new instance
384
+ * @returns true if headroom is sufficient
385
+ */
386
+ canAccommodateInstance(estimatedEntities?: number): boolean;
387
+ /**
388
+ * **Piece 8 commit 2 — observed per-entity RAM estimate.** When
389
+ * any subscriber has reported native memory via
390
+ * {@link reportNativeMemory}, the ratio of total reported bytes
391
+ * to total tracked entities is the actual observed per-entity
392
+ * footprint — accurate for THIS deployment's workload mix instead
393
+ * of a one-size-fits-all guess. Falls back to
394
+ * {@link FALLBACK_BYTES_PER_ENTITY} when subscribers haven't
395
+ * reported (cold boot, no instances tracked, or workloads where
396
+ * the brain hasn't done any reading yet).
397
+ *
398
+ * Floored at {@link MIN_BYTES_PER_ENTITY} so a subscriber that
399
+ * temporarily under-reports during a flush doesn't make
400
+ * `canAccommodateInstance` say "yes" to instances that can't fit.
401
+ *
402
+ * Cheap call: O(subscribers + instances) — both bounded by a few
403
+ * dozen in production. Fine to invoke per trackInstance /
404
+ * canAccommodateInstance call.
405
+ */
406
+ private observedBytesPerEntity;
407
+ /**
408
+ * Get the scope key of the instance that should be evicted first.
409
+ * Uses weighted scoring: large + idle instances are evicted before
410
+ * small + active ones.
411
+ *
412
+ * @returns The key to evict, or null if no instances are tracked
413
+ */
414
+ getEvictionCandidate(): string | null;
415
+ /**
416
+ * Rebalance cache sizing based on current instance count and memory state.
417
+ *
418
+ * Three responsibilities:
419
+ * 1. Recompute the total cache budget from instance count + system
420
+ * memory (the pre-Piece-8 behaviour).
421
+ * 2. Fire the back-compat `onCacheResize` if it exists.
422
+ * 3. Notify every per-category subscriber of its current
423
+ * `allowedBytes` ceiling — picks up any whole-pool shift even
424
+ * when the per-category dynamic adaptation isn't pressed.
425
+ */
426
+ private rebalance;
427
+ /**
428
+ * Drive one tick of the per-category dynamic rebalance. Reads the
429
+ * latest `reportNativeMemory` values, identifies over- and under-
430
+ * utilized categories, shifts the dynamic shares between them
431
+ * (bounded by `CATEGORY_MIN_SHARE` / `CATEGORY_MAX_SHARE`), and
432
+ * notifies any subscriber whose allowance changed meaningfully.
433
+ *
434
+ * Auto-stops the pressure-tick loop after 3 consecutive quiet ticks
435
+ * (no category > low-water).
436
+ */
437
+ private runPressureTick;
438
+ /**
439
+ * Shift dynamic share from donor categories to recipient categories.
440
+ *
441
+ * Both sides are PRE-CAPPED by the per-category min/max share bounds
442
+ * (25% / 200% of base share, respectively) BEFORE the proportional
443
+ * distribution runs. Pre-capping is the only safe pattern: clamping
444
+ * after a proportional split would either (a) lose capacity (donors
445
+ * give shares that recipients can't accept due to their ceiling) or
446
+ * (b) require renormalization, which silently violates the ceiling.
447
+ *
448
+ * `transfer = min(total bounded contribution, total bounded demand)`
449
+ * — by construction the per-donor `give` and per-recipient `take`
450
+ * sums are equal, so the shares always sum to 1.0 without
451
+ * renormalization. The float drift over many cycles is bounded by
452
+ * the floating-point precision of a single division, well under any
453
+ * test's threshold.
454
+ */
455
+ private rebalanceShares;
456
+ /**
457
+ * Compute the current allowed-bytes ceiling for a category.
458
+ */
459
+ private computeAllowedBytes;
460
+ /**
461
+ * Sum the reportedBytes of every subscriber by category.
462
+ */
463
+ private aggregateReportedBytesByCategory;
464
+ /**
465
+ * Notify every subscriber of its current allowed-bytes ceiling. Fires
466
+ * the callback only when the allowance has shifted by more than the
467
+ * `NOTIFY_DELTA` threshold from the last delivered value — avoids
468
+ * spurious wakeups on small re-rebalances.
469
+ */
470
+ private notifySubscribers;
471
+ /**
472
+ * Normalize categoryShares so they sum to exactly 1.0. Compensates for
473
+ * float drift over many rebalance cycles. Preserves the relative
474
+ * proportions of the four categories.
475
+ */
476
+ private renormalizeShares;
477
+ /**
478
+ * Compute the recommended cache budget given current instance memory usage.
479
+ *
480
+ * Formula: min(MAX_CACHE_RATIO × available, available - fixed - instances - headroom)
481
+ * Clamped to MIN_CACHE_BYTES.
482
+ */
483
+ private computeCacheBudget;
484
+ /**
485
+ * Determine memory pressure level from current RSS.
486
+ */
487
+ private computePressure;
488
+ private getTotalEntities;
489
+ private getTotalInstanceMemory;
490
+ }
491
+ //# sourceMappingURL=ResourceManager.d.ts.map