grepmax 0.26.59 → 0.26.61

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.59",
13
+ "version": "0.26.61",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -453,6 +453,7 @@ fixtures/
453
453
  | --- | --- | --- |
454
454
  | `GMAX_EMBED_MODE` | Force `cpu` or `gpu` | Auto-detect |
455
455
  | `GMAX_WORKER_THREADS` | Worker processes for embedding; overrides `gmax config --worker-threads` | `min(cores, 4, max(2, floor(cores/2)))` |
456
+ | `GMAX_RESOURCE_BUDGET_MB` | macOS aggregate admission budget across clients, stores, workers and models; accepts 256–6144 MiB | `6144` |
456
457
  | `GMAX_WORKER_RSS_RECYCLE_MB` | Recycle workers that remain above this RSS; `0` disables the check | `1536` |
457
458
  | `GMAX_DEBUG` | Debug logging | Off |
458
459
  | `GMAX_RERANK` | Force ColBERT rerank (`1`); concentrated candidates can also enable it automatically ([why](docs/known-limitations.md)) | Off |
@@ -552,3 +553,20 @@ grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai
552
553
  ## License
553
554
 
554
555
  Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).
556
+
557
+ On macOS, heavy work uses a shared resource ledger and a bounded batch footprint probe covering every gmax process and its children. Native stores, workers and model launches reserve headroom before allocating it. Warning or unknown measurements pause heavy work; known critical pressure persists a stop. The 6 GiB admission budget is a conservative scheduling threshold, not an OS-enforced memory cap. It may be lowered with `GMAX_RESOURCE_BUDGET_MB`. Existing safety markers remain in place after upgrades.
558
+
559
+ MCP sessions started before this resource policy must reconnect after upgrading before heavy work can resume. Keyword search through the bounded read service stays available while those sessions are running. Cached resource snapshots in `gmax status --json` distinguish aggregate physical footprint from daemon RSS.
560
+
561
+ ### Host guard policy
562
+
563
+ `hostGuardPolicy: "critical-only"` in `~/.gmax/config.json` (or
564
+ `GMAX_HOST_GUARD_POLICY=critical-only`) selects an explicit availability policy.
565
+ Warning and unavailable pressure probes remain diagnostic and do not pause
566
+ semantic search or indexing. Confirmed critical OS or kernel pressure still
567
+ persists a safety stop and shuts down. This mode disables aggregate memory
568
+ admission and the requirement to reconnect older MCP clients; native cache and
569
+ worker limits remain. The default `strict` policy keeps aggregate admission.
570
+ Restart the daemon after changing policy. An existing safety-stop marker requires
571
+ explicit operator review and clearing; selecting a policy never clears it.
572
+ Full-table compaction remains disabled in either policy.
@@ -101,6 +101,7 @@ const mcp_execution_1 = require("../lib/utils/mcp-execution");
101
101
  const path_containment_1 = require("../lib/utils/path-containment");
102
102
  const project_registry_1 = require("../lib/utils/project-registry");
103
103
  const project_root_1 = require("../lib/utils/project-root");
104
+ const resource_budget_1 = require("../lib/utils/resource-budget");
104
105
  const scope_filter_1 = require("../lib/utils/scope-filter");
105
106
  const store_access_1 = require("../lib/utils/store-access");
106
107
  const store_context_1 = require("../lib/utils/store-context");
@@ -368,6 +369,18 @@ exports.mcp = new commander_1.Command("mcp")
368
369
  .description("Start MCP server (stdio, auto-started by plugins)")
369
370
  .action((_optsArg, _cmd) => __awaiter(void 0, void 0, void 0, function* () {
370
371
  process.title = "gmax-mcp";
372
+ try {
373
+ const client = resource_budget_1.resourceBudget.registerClient();
374
+ process.once("exit", () => {
375
+ try {
376
+ client.release();
377
+ }
378
+ catch (_a) { }
379
+ });
380
+ }
381
+ catch (error) {
382
+ console.error(`gmax: resource coordination unavailable; heavy work paused (${error instanceof Error ? error.message : "unknown"})`);
383
+ }
371
384
  // --- Lifecycle ---
372
385
  // No per-session VectorDB. Every read tool goes through the daemon's read
373
386
  // verbs, and the only store this process may open is the short-lived one
@@ -67,6 +67,8 @@ const autostart_1 = require("../utils/autostart");
67
67
  const blocked_roots_1 = require("../utils/blocked-roots");
68
68
  const daemon_client_1 = require("../utils/daemon-client");
69
69
  const daemon_launcher_1 = require("../utils/daemon-launcher");
70
+ const host_guard_policy_1 = require("../utils/host-guard-policy");
71
+ const host_resource_1 = require("../utils/host-resource");
70
72
  const kernel_zone_1 = require("../utils/kernel-zone");
71
73
  const keyed_mutex_1 = require("../utils/keyed-mutex");
72
74
  const log_rotate_1 = require("../utils/log-rotate");
@@ -76,6 +78,7 @@ const process_1 = require("../utils/process");
76
78
  const process_footprint_1 = require("../utils/process-footprint");
77
79
  const project_registry_1 = require("../utils/project-registry");
78
80
  const query_timeout_1 = require("../utils/query-timeout");
81
+ const resource_budget_1 = require("../utils/resource-budget");
79
82
  const resource_snapshot_1 = require("../utils/resource-snapshot");
80
83
  const root_availability_1 = require("../utils/root-availability");
81
84
  const safety_latch_1 = require("../utils/safety-latch");
@@ -165,6 +168,7 @@ class Daemon {
165
168
  // resources must be gated on this to avoid hitting null stores mid-startup.
166
169
  this.ready = false;
167
170
  this.pausedReason = null;
171
+ this.lastHostResources = null;
168
172
  this.pausePromise = null;
169
173
  this.pausedReadGate = new async_semaphore_1.AsyncSemaphore(1);
170
174
  this.processManager = new process_manager_1.ProcessManager({
@@ -246,7 +250,7 @@ class Daemon {
246
250
  }
247
251
  start() {
248
252
  return __awaiter(this, arguments, void 0, function* (options = {}) {
249
- var _a, _b;
253
+ var _a, _b, _c;
250
254
  const denied = (0, autostart_1.daemonStartDeniedReason)();
251
255
  if (denied !== null && !options.readOnly)
252
256
  throw new Error(`gmax: ${denied}; preserving containment`);
@@ -260,6 +264,20 @@ class Daemon {
260
264
  this.pausedReason = (_a = denied !== null && denied !== void 0 ? denied : pressure === null || pressure === void 0 ? void 0 : pressure.reason) !== null && _a !== void 0 ? _a : "read-only service requested";
261
265
  }
262
266
  process.title = "gmax-daemon";
267
+ console.log(`[daemon] Host guard policy: ${(0, host_guard_policy_1.hostGuardPolicy)()}`);
268
+ if (this.pausedReason === null) {
269
+ try {
270
+ this.lastHostResources = resource_budget_1.resourceBudget.check();
271
+ }
272
+ catch (error) {
273
+ if (error instanceof resource_budget_1.ResourceAdmissionError && error.critical)
274
+ throw error;
275
+ this.pausedReason =
276
+ error instanceof Error
277
+ ? error.message
278
+ : "aggregate resource admission unavailable";
279
+ }
280
+ }
263
281
  // Read verbs register here rather than at read-verbs.ts module scope: the
264
282
  // registry is asserted on exactly in tests/ipc-read-verbs.test.ts, and a
265
283
  // module-scope registration would make it non-empty on import. Register
@@ -311,7 +329,7 @@ class Daemon {
311
329
  try {
312
330
  fs.unlinkSync(config_1.PATHS.daemonSocket);
313
331
  }
314
- catch (_c) { }
332
+ catch (_d) { }
315
333
  this.server = net.createServer((conn) => {
316
334
  (0, logger_1.debug)("daemon", "client connected");
317
335
  this.connections.add(conn);
@@ -405,9 +423,13 @@ class Daemon {
405
423
  }
406
424
  catch (error) {
407
425
  const freshPressure = this.hostPressureDeniedReason();
408
- if (freshPressure === null || freshPressure.critical)
426
+ if (freshPressure === null || freshPressure === void 0 ? void 0 : freshPressure.critical)
427
+ throw error;
428
+ if (freshPressure === null &&
429
+ !(error instanceof resource_budget_1.ResourceAdmissionError) &&
430
+ (error === null || error === void 0 ? void 0 : error.code) !== "HOST_SAFETY")
409
431
  throw error;
410
- this.pauseWork(freshPressure.reason);
432
+ this.pauseWork((_b = freshPressure === null || freshPressure === void 0 ? void 0 : freshPressure.reason) !== null && _b !== void 0 ? _b : error.message);
411
433
  yield this.pausePromise;
412
434
  }
413
435
  if (this.pausedReason === null) {
@@ -427,7 +449,7 @@ class Daemon {
427
449
  // 6. LLM server manager (constructed, not started — starts on first request)
428
450
  this.llmServer = new server_1.LlmServer();
429
451
  // 6b. MLX embed server — start if GPU mode is active
430
- const globalConfig = (_b = this.activeConfig) !== null && _b !== void 0 ? _b : (0, index_config_1.readGlobalConfig)();
452
+ const globalConfig = (_c = this.activeConfig) !== null && _c !== void 0 ? _c : (0, index_config_1.readGlobalConfig)();
431
453
  const isAppleSilicon = process.arch === "arm64" && process.platform === "darwin";
432
454
  if (this.pausedReason === null &&
433
455
  isAppleSilicon &&
@@ -510,6 +532,8 @@ class Daemon {
510
532
  this.logResourceSnapshot("paused-heartbeat", null);
511
533
  return;
512
534
  }
535
+ if (!this.checkAggregateResources())
536
+ return;
513
537
  // Every 5 ticks (5 min), probe the MLX embed server and respawn if
514
538
  // it's gone zombie (port held but /health unresponsive). Closes the
515
539
  // 42h-degradation window where workers silently fell back to ONNX CPU
@@ -758,6 +782,7 @@ class Daemon {
758
782
  });
759
783
  }
760
784
  assertHeavyOperationAdmission(name) {
785
+ var _a;
761
786
  const denied = (0, autostart_1.daemonStartDeniedReason)();
762
787
  if (denied !== null)
763
788
  throw new Error(`${name} refused: ${denied}`);
@@ -765,6 +790,8 @@ class Daemon {
765
790
  if (!this.checkKernelZonePressure()) {
766
791
  throw new Error(`${name} refused: host pressure is unsafe or unknown${this.pausedReason ? `: ${this.pausedReason}` : ""}`);
767
792
  }
793
+ if (!this.checkAggregateResources())
794
+ throw new Error(`${name} refused: ${(_a = this.pausedReason) !== null && _a !== void 0 ? _a : "aggregate resources unavailable"}`);
768
795
  // Another process can quarantine the machine while the probes run.
769
796
  const finalDenied = (0, autostart_1.daemonStartDeniedReason)();
770
797
  if (finalDenied !== null)
@@ -2037,6 +2064,8 @@ class Daemon {
2037
2064
  // Both ordinary probes and deferred-recycle ticks check safety first.
2038
2065
  if (!this.checkKernelZonePressure())
2039
2066
  return;
2067
+ if (sampleFootprint && !this.checkAggregateResources())
2068
+ return;
2040
2069
  this.maybeRecycle(sampleFootprint);
2041
2070
  if (this.shuttingDown || this.recycling || !sampleFootprint)
2042
2071
  return;
@@ -2048,6 +2077,8 @@ class Daemon {
2048
2077
  logResourceSnapshot(reason, footprintMb) {
2049
2078
  var _a, _b;
2050
2079
  var _c, _d;
2080
+ if (reason.startsWith("paused-"))
2081
+ this.lastHostResources = (0, host_resource_1.sampleHostResources)();
2051
2082
  let lanceCacheBytes = null;
2052
2083
  try {
2053
2084
  lanceCacheBytes = (_c = (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.cacheSizeBytes()) !== null && _c !== void 0 ? _c : null;
@@ -2061,6 +2092,7 @@ class Daemon {
2061
2092
  pendingFiles: [...this.processors.values()].reduce((sum, processor) => sum + processor.progress.pendingFiles, 0),
2062
2093
  operations: this.operations.activeCount,
2063
2094
  maintenance: (_d = (_b = this.vectorDb) === null || _b === void 0 ? void 0 : _b.isMaintenanceActive()) !== null && _d !== void 0 ? _d : false,
2095
+ host: this.lastHostResources,
2064
2096
  });
2065
2097
  console.log(`[daemon] Resource snapshot: ${JSON.stringify(this.lastResourceSnapshot)}`);
2066
2098
  }
@@ -2165,6 +2197,24 @@ class Daemon {
2165
2197
  }
2166
2198
  return this.pausedReason === null;
2167
2199
  }
2200
+ checkAggregateResources() {
2201
+ if (this.shuttingDown || this.recycling || this.pausedReason !== null)
2202
+ return false;
2203
+ try {
2204
+ this.lastHostResources = resource_budget_1.resourceBudget.check();
2205
+ return true;
2206
+ }
2207
+ catch (error) {
2208
+ const reason = error instanceof Error
2209
+ ? error.message
2210
+ : "aggregate resource admission unavailable";
2211
+ if (error instanceof resource_budget_1.ResourceAdmissionError && error.critical)
2212
+ this.stopForSafety(reason);
2213
+ else
2214
+ this.pauseWork(reason);
2215
+ return false;
2216
+ }
2217
+ }
2168
2218
  /** Unknown/warning pauses resource expansion; only known critical shuts down. */
2169
2219
  hostPressureDeniedReason() {
2170
2220
  if (process.platform !== "darwin")
@@ -2191,6 +2241,8 @@ class Daemon {
2191
2241
  measured: true,
2192
2242
  critical: true,
2193
2243
  };
2244
+ if ((0, host_guard_policy_1.hostGuardPolicy)() === "critical-only")
2245
+ return null;
2194
2246
  for (const sample of [memory, freshMemory]) {
2195
2247
  if (sample.status !== "known")
2196
2248
  return {
@@ -52,6 +52,7 @@ const config_1 = require("../../config");
52
52
  const log_rotate_1 = require("../utils/log-rotate");
53
53
  const mlx_hf_cache_1 = require("../utils/mlx-hf-cache");
54
54
  const process_1 = require("../utils/process");
55
+ const resource_budget_1 = require("../utils/resource-budget");
55
56
  const STARTUP_ATTEMPTS = 30;
56
57
  const STARTUP_POLL_MS = 1000;
57
58
  const MAX_HEALTH_BODY_BYTES = 16 * 1024;
@@ -151,11 +152,18 @@ class MlxServerManager {
151
152
  terminateOwned(server) {
152
153
  return __awaiter(this, void 0, void 0, function* () {
153
154
  var _a;
154
- const terminate = (_a = this.deps.terminateGroup) !== null && _a !== void 0 ? _a : process_1.terminateProcessGroup;
155
+ var _b;
156
+ const terminate = (_b = this.deps.terminateGroup) !== null && _b !== void 0 ? _b : process_1.terminateProcessGroup;
155
157
  const stopped = yield terminate(server.pid);
156
158
  if (!stopped) {
157
159
  console.error(`[daemon] Failed to fully stop owned MLX process group ${server.pid}`);
158
160
  }
161
+ else {
162
+ try {
163
+ (_a = server.reservation) === null || _a === void 0 ? void 0 : _a.release();
164
+ }
165
+ catch (_c) { }
166
+ }
159
167
  server.child.removeAllListeners("error");
160
168
  server.child.removeAllListeners("exit");
161
169
  if (this.stable === server)
@@ -204,7 +212,7 @@ class MlxServerManager {
204
212
  ensureInternal(model) {
205
213
  return __awaiter(this, void 0, void 0, function* () {
206
214
  var _a, _b, _c, _d, _e;
207
- var _f, _g, _h, _j, _k;
215
+ var _f, _g, _h, _j, _k, _l;
208
216
  this.phase = "probing";
209
217
  const health = yield this.probeHealth(this.port);
210
218
  if (this.shouldStop())
@@ -223,6 +231,7 @@ class MlxServerManager {
223
231
  this.stable.lastHealthyAt = Date.now();
224
232
  return;
225
233
  }
234
+ resource_budget_1.resourceBudget.check(this.getPortPid(this.port));
226
235
  if (this.stable.kind === "owned")
227
236
  yield this.terminateOwned(this.stable);
228
237
  this.stable = { kind: "adopted", model, lastHealthyAt: Date.now() };
@@ -278,17 +287,41 @@ class MlxServerManager {
278
287
  const logFd = openLog(path.join(config_1.PATHS.logsDir, "mlx-embed-server.log"));
279
288
  const env = Object.assign(Object.assign({}, process.env), { MLX_EMBED_MODEL: model, GMAX_EMBED_OWNER_TOKEN: owner, HF_HOME: (0, mlx_hf_cache_1.resolveMlxHfHome)(model) });
280
289
  this.phase = "starting";
281
- let child;
290
+ let child = null;
291
+ let reservation;
282
292
  try {
293
+ reservation = resource_budget_1.resourceBudget.reserve(resource_budget_1.EMBEDDING_RESOURCE_RESERVE_MB, "embedding");
283
294
  child = ((_j = this.deps.spawn) !== null && _j !== void 0 ? _j : node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
284
295
  cwd: serverDir,
285
296
  detached: true,
286
297
  stdio: ["ignore", logFd, logFd],
287
298
  env,
288
299
  });
300
+ if (!child.pid) {
301
+ reservation.release();
302
+ throw new Error("spawned MLX process has no PID");
303
+ }
304
+ try {
305
+ reservation.attach(child.pid, true);
306
+ }
307
+ catch (error) {
308
+ const stopped = yield ((_k = this.deps.terminateGroup) !== null && _k !== void 0 ? _k : process_1.terminateProcessGroup)(child.pid);
309
+ if (stopped)
310
+ reservation.release();
311
+ throw error;
312
+ }
289
313
  }
290
314
  catch (error) {
315
+ // If no child was created, there is no native group to keep charged.
316
+ if (!child) {
317
+ try {
318
+ reservation === null || reservation === void 0 ? void 0 : reservation.release();
319
+ }
320
+ catch (_m) { }
321
+ }
291
322
  this.lastError = error instanceof Error ? error.message : String(error);
323
+ if (error instanceof resource_budget_1.ResourceAdmissionError)
324
+ this.enabled = false;
292
325
  this.phase = "failed";
293
326
  console.error(`[daemon] MLX embed server failed to spawn: ${this.lastError} — falling back to CPU embeddings`);
294
327
  return;
@@ -297,9 +330,9 @@ class MlxServerManager {
297
330
  try {
298
331
  closeFd(logFd);
299
332
  }
300
- catch (_l) { }
333
+ catch (_o) { }
301
334
  }
302
- if (!child.pid) {
335
+ if (!(child === null || child === void 0 ? void 0 : child.pid)) {
303
336
  this.lastError = "spawned MLX process has no PID";
304
337
  this.phase = "failed";
305
338
  return;
@@ -312,6 +345,7 @@ class MlxServerManager {
312
345
  model,
313
346
  owner,
314
347
  lastHealthyAt: 0,
348
+ reservation,
315
349
  };
316
350
  const startup = { error: null, exited: false };
317
351
  const onError = (error) => {
@@ -338,6 +372,7 @@ class MlxServerManager {
338
372
  child.off("exit", onExit);
339
373
  if (readiness.owner !== owner) {
340
374
  yield this.terminateOwned(candidate);
375
+ resource_budget_1.resourceBudget.check(this.getPortPid(this.port));
341
376
  this.stable = {
342
377
  kind: "adopted",
343
378
  model,
@@ -356,7 +391,7 @@ class MlxServerManager {
356
391
  child.off("error", onError);
357
392
  child.off("exit", onExit);
358
393
  yield this.terminateOwned(candidate);
359
- this.lastError = (_k = (_e = startup.error) === null || _e === void 0 ? void 0 : _e.message) !== null && _k !== void 0 ? _k : (this.shouldStop() ? "MLX startup cancelled" : "MLX startup timed out");
394
+ this.lastError = (_l = (_e = startup.error) === null || _e === void 0 ? void 0 : _e.message) !== null && _l !== void 0 ? _l : (this.shouldStop() ? "MLX startup cancelled" : "MLX startup timed out");
360
395
  if (!this.shouldStop()) {
361
396
  this.phase = "failed";
362
397
  console.error(`[daemon] ${this.lastError} — falling back to CPU embeddings`);
@@ -50,6 +50,8 @@ const path = __importStar(require("node:path"));
50
50
  const config_1 = require("../../config");
51
51
  const index_config_1 = require("../index/index-config");
52
52
  const log_rotate_1 = require("../utils/log-rotate");
53
+ const process_1 = require("../utils/process");
54
+ const resource_budget_1 = require("../utils/resource-budget");
53
55
  const config_2 = require("./config");
54
56
  // ⛔ HARD STOP FOR AI AGENTS: This starts llama-server with a multi-GB local
55
57
  // LLM (Qwen3.5-35B GGUF ≈21GB by default). DO NOT start it — via start(),
@@ -124,6 +126,7 @@ class LlmServer {
124
126
  throw new Error("LLM is disabled. Run `gmax llm on` to enable.");
125
127
  }
126
128
  if (yield this.healthy()) {
129
+ resource_budget_1.resourceBudget.check(this.readPid());
127
130
  // Adopt an existing server (e.g. after daemon crash + restart)
128
131
  this.lastRequestTime = Date.now();
129
132
  this.startTime = Date.now();
@@ -148,7 +151,17 @@ class LlmServer {
148
151
  if (!fs.existsSync(this.config.model)) {
149
152
  throw new Error(`Model file not found: "${this.config.model}". Set GMAX_LLM_MODEL to a valid .gguf path`);
150
153
  }
151
- const logFd = (0, log_rotate_1.openRotatedLog)(config_1.PATHS.llmLogFile);
154
+ // Reserve the model's on-disk size plus runtime headroom before starting
155
+ // it. The default multi-GB model cannot fit the containment budget.
156
+ const reservation = resource_budget_1.resourceBudget.reserve(Math.ceil(fs.statSync(this.config.model).size / 1048576) + 512, "llm");
157
+ let logFd;
158
+ try {
159
+ logFd = (0, log_rotate_1.openRotatedLog)(config_1.PATHS.llmLogFile);
160
+ }
161
+ catch (error) {
162
+ reservation.release();
163
+ throw error;
164
+ }
152
165
  const args = [
153
166
  "-m",
154
167
  this.config.model,
@@ -164,16 +177,36 @@ class LlmServer {
164
177
  if (this.config.reasoningFormat) {
165
178
  args.push("--reasoning-format", this.config.reasoningFormat);
166
179
  }
167
- const child = (0, node_child_process_1.spawn)(binary, args, {
168
- detached: true,
169
- stdio: ["ignore", logFd, logFd],
170
- });
180
+ let child;
181
+ try {
182
+ child = (0, node_child_process_1.spawn)(binary, args, {
183
+ detached: true,
184
+ stdio: ["ignore", logFd, logFd],
185
+ });
186
+ }
187
+ catch (error) {
188
+ reservation.release();
189
+ throw error;
190
+ }
191
+ finally {
192
+ fs.closeSync(logFd);
193
+ }
171
194
  child.unref();
172
- fs.closeSync(logFd);
173
195
  const pid = child.pid;
174
196
  if (!pid) {
197
+ reservation.release();
175
198
  throw new Error("Failed to spawn llama-server — no PID returned");
176
199
  }
200
+ // Group reservations are reaped only after the whole detached group is
201
+ // confirmed dead. A launcher exit cannot make surviving children free.
202
+ try {
203
+ reservation.attach(pid, true);
204
+ }
205
+ catch (error) {
206
+ if (yield (0, process_1.terminateProcessGroup)(pid))
207
+ reservation.release();
208
+ throw error;
209
+ }
177
210
  fs.writeFileSync(config_1.PATHS.llmPidFile, String(pid));
178
211
  console.log(`[llm] Starting llama-server (PID: ${pid}, port: ${this.config.port})`);
179
212
  // Poll until ready
@@ -257,6 +290,7 @@ class LlmServer {
257
290
  throw new Error("LLM is disabled. Run `gmax llm on` to enable.");
258
291
  }
259
292
  if (yield this.healthy()) {
293
+ resource_budget_1.resourceBudget.check(this.readPid());
260
294
  this.touchIdle();
261
295
  return;
262
296
  }
@@ -63,6 +63,7 @@ const cleanup_1 = require("../utils/cleanup");
63
63
  const filter_builder_1 = require("../utils/filter-builder");
64
64
  const logger_1 = require("../utils/logger");
65
65
  const query_timeout_1 = require("../utils/query-timeout");
66
+ const resource_budget_1 = require("../utils/resource-budget");
66
67
  const ann_config_1 = require("./ann-config");
67
68
  const compaction_result_1 = require("./compaction-result");
68
69
  const lancedb = __importStar(require("./lance-sdk"));
@@ -252,6 +253,8 @@ class VectorDB {
252
253
  this.lastDiskCheckMs = 0;
253
254
  this.lastLoggedPressure = "ok";
254
255
  this.leasePromise = null;
256
+ this.resourceReservation = null;
257
+ this.connecting = null;
255
258
  this.leaseAbort = new AbortController();
256
259
  this.closePromise = null;
257
260
  this.releaseLeaseOnClose = true;
@@ -374,6 +377,8 @@ class VectorDB {
374
377
  }
375
378
  getDb() {
376
379
  return __awaiter(this, void 0, void 0, function* () {
380
+ var _a;
381
+ var _b, _c;
377
382
  if (this.closed) {
378
383
  throw new Error("VectorDB connection is closed");
379
384
  }
@@ -387,17 +392,38 @@ class VectorDB {
387
392
  throw new Error("VectorDB connection is closed");
388
393
  }
389
394
  if (!this.db) {
395
+ if (this.connecting)
396
+ return this.connecting;
390
397
  if (creatingStore || !fs.existsSync(this.lancedbDir)) {
391
398
  this.assertMutationAllowed();
392
399
  (0, maintenance_policy_1.assertFreshDiskMutationAllowed)(this.lancedbDir);
393
400
  }
394
401
  fs.mkdirSync(this.lancedbDir, { recursive: true });
402
+ // The explicit bounded reader remains available under warning/unknown
403
+ // pressure. All ordinary clients reserve their native cache capacity in
404
+ // one shared ledger before opening a connection, including offline MCP.
405
+ if (!this.options.readOnly) {
406
+ this.resourceReservation = resource_budget_1.resourceBudget.reserve(((_b = this.options.indexCacheMb) !== null && _b !== void 0 ? _b : LANCE_INDEX_CACHE_MB) +
407
+ ((_c = this.options.metadataCacheMb) !== null && _c !== void 0 ? _c : LANCE_METADATA_CACHE_MB), "native-store");
408
+ }
395
409
  this.session = createLanceSession(this.options);
396
410
  // 0.38 accepts the legacy third argument in its types but drops it in
397
411
  // the JS wrapper. Put the session in native ConnectionOptions instead.
398
- this.db = yield lancedb.connect(this.lancedbDir, {
412
+ this.connecting = lancedb.connect(this.lancedbDir, {
399
413
  session: this.session,
400
414
  });
415
+ try {
416
+ this.db = yield this.connecting;
417
+ }
418
+ catch (error) {
419
+ (_a = this.resourceReservation) === null || _a === void 0 ? void 0 : _a.release();
420
+ this.resourceReservation = null;
421
+ this.session = null;
422
+ throw error;
423
+ }
424
+ finally {
425
+ this.connecting = null;
426
+ }
401
427
  }
402
428
  return this.db;
403
429
  });
@@ -1874,7 +1900,8 @@ class VectorDB {
1874
1900
  }
1875
1901
  finishClose() {
1876
1902
  return __awaiter(this, void 0, void 0, function* () {
1877
- var _a, _b;
1903
+ var _a, _b, _c;
1904
+ yield (_a = this.connecting) === null || _a === void 0 ? void 0 : _a.catch(() => { });
1878
1905
  if (this.maintenanceTimer) {
1879
1906
  clearInterval(this.maintenanceTimer);
1880
1907
  this.maintenanceTimer = null;
@@ -1893,12 +1920,22 @@ class VectorDB {
1893
1920
  ]);
1894
1921
  this.maintenancePromise = null;
1895
1922
  }
1896
- (_a = this.unregisterCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
1923
+ (_b = this.unregisterCleanup) === null || _b === void 0 ? void 0 : _b.call(this);
1897
1924
  this.unregisterCleanup = undefined;
1898
1925
  let closeError;
1899
- if ((_b = this.db) === null || _b === void 0 ? void 0 : _b.close) {
1926
+ let nativeClosed = !this.db;
1927
+ if ((_c = this.db) === null || _c === void 0 ? void 0 : _c.close) {
1900
1928
  try {
1901
1929
  const closing = this.db.close();
1930
+ void Promise.resolve(closing).then(() => {
1931
+ var _a;
1932
+ nativeClosed = true;
1933
+ try {
1934
+ (_a = this.resourceReservation) === null || _a === void 0 ? void 0 : _a.release();
1935
+ this.resourceReservation = null;
1936
+ }
1937
+ catch (_b) { }
1938
+ }, () => { });
1902
1939
  if (this.requireClosedOnClose)
1903
1940
  yield closing;
1904
1941
  else {
@@ -1914,12 +1951,21 @@ class VectorDB {
1914
1951
  }
1915
1952
  this.db = null;
1916
1953
  this.session = null;
1954
+ if (nativeClosed && this.resourceReservation) {
1955
+ try {
1956
+ this.resourceReservation.release();
1957
+ this.resourceReservation = null;
1958
+ }
1959
+ catch (error) {
1960
+ closeError !== null && closeError !== void 0 ? closeError : (closeError = error);
1961
+ }
1962
+ }
1917
1963
  if (this.leasePromise) {
1918
1964
  let lease = null;
1919
1965
  try {
1920
1966
  lease = yield this.leasePromise;
1921
1967
  }
1922
- catch (_c) {
1968
+ catch (_d) {
1923
1969
  this.leasePromise = null;
1924
1970
  }
1925
1971
  if (lease) {
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hostGuardPolicy = hostGuardPolicy;
37
+ exports.sampleCriticalPressure = sampleCriticalPressure;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const config_1 = require("../../config");
41
+ const kernel_zone_1 = require("./kernel-zone");
42
+ /** Explicit operator preference. Unknown values preserve strict admission. */
43
+ function hostGuardPolicy() {
44
+ const environment = process.env.GMAX_HOST_GUARD_POLICY;
45
+ if (environment !== undefined)
46
+ return environment === "critical-only" ? "critical-only" : "strict";
47
+ try {
48
+ const config = JSON.parse(fs.readFileSync(path.join(config_1.PATHS.sharedRoot, "config.json"), "utf8"));
49
+ return config.hostGuardPolicy === "critical-only"
50
+ ? "critical-only"
51
+ : "strict";
52
+ }
53
+ catch (_a) {
54
+ return "strict";
55
+ }
56
+ }
57
+ /** No footprint scan or reservation ledger in explicitly selected critical-only
58
+ * mode. Unknown probes stay visible as unknown; they are never called healthy. */
59
+ function sampleCriticalPressure() {
60
+ const at = Date.now();
61
+ const memory = (0, kernel_zone_1.probeMemoryPressure)();
62
+ const kernel = (0, kernel_zone_1.probeKernelZoneUsage)();
63
+ const freshMemory = (0, kernel_zone_1.probeMemoryPressure)();
64
+ const memoryPressure = [memory, freshMemory].some((p) => p.status === "known" && p.pressure === "critical")
65
+ ? "critical"
66
+ : freshMemory.status === "known"
67
+ ? freshMemory.pressure
68
+ : "unknown";
69
+ return {
70
+ at,
71
+ completedAt: Date.now(),
72
+ platform: process.platform,
73
+ processes: [],
74
+ aggregateFootprintMb: null,
75
+ physicalFreeMb: null,
76
+ swapUsedMb: null,
77
+ memoryPressure,
78
+ kernelPressure: kernel.status === "known" ? kernel.usage.pressure : "unknown",
79
+ kernelBytes: kernel.status === "known" ? kernel.usage.bytes : null,
80
+ incompleteReasons: [
81
+ "aggregate admission disabled by explicit critical-only policy",
82
+ ],
83
+ };
84
+ }