grepmax 0.26.53 → 0.26.55

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.53",
13
+ "version": "0.26.55",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -54,9 +54,11 @@ gmax config # View current settings
54
54
  gmax config --embed-mode gpu # Switch to GPU (Apple Silicon)
55
55
  gmax config --worker-threads 2 # Cap worker processes (auto = default); applies on daemon restart
56
56
  gmax doctor # Health check
57
- gmax doctor --fix # Auto-repair (compact, prune, remove stale locks)
57
+ gmax doctor --fix # Repair checks; full-table maintenance is disabled
58
58
  ```
59
59
 
60
+ **Storage containment:** full-table compaction is disabled, including forced maintenance and doctor repair. Existing retained index copies are not reclaimed by this release. The verified prune-only recovery path is separate work; normal incremental indexing remains available when the host is healthy and not quarantined. A persistent host safety stop blocks mutation and daemon restart. Installing an update preserves existing quarantine.
61
+
60
62
  ### Core Commands
61
63
 
62
64
  ```bash
@@ -234,7 +236,7 @@ gmax watch restart # Stop it, wait for it to exit, start a fresh one
234
236
  gmax status # See all projects + watcher status
235
237
  ```
236
238
 
237
- Adding or indexing a project can start the daemon automatically. MCP acquires primary-project watch leases lazily when an index-reading tool is used; discovery and catalog listing start no background work. It shuts down after 4 hours of inactivity, and hands off to a fresh daemon after 24 hours or once its memory footprint passes 2.5 GB. File-change batches are processed on up to four worker processes by default, added only when work backs up, with one kept free for searches; LanceDB compaction runs after writes rather than on every maintenance tick.
239
+ Adding or indexing a project can start the daemon automatically. MCP acquires primary-project watch leases lazily when an index-reading tool is used; discovery and catalog listing start no background work. It shuts down after 4 hours of inactivity, and hands off to a fresh daemon after 24 hours or once its memory footprint passes 2.5 GB. File-change batches are processed on up to four worker processes by default, added only when work backs up, with one kept free for searches; full-table compaction and new FTS/ANN index builds are disabled by containment. Existing indexes remain readable. Host safety stops persist across restart and block heavy operations.
238
240
 
239
241
  `gmax watch status` reports failed files, native watcher recovery or polling, dropped-event counts, and the last complete filesystem reconciliation. Polling scans run every five minutes, so recent edits can lag. Search and MCP responses retain health warnings even after the file queue drains. A complete reconciliation means the filesystem scan completed; queued embedding work can still be pending.
240
242
 
@@ -461,7 +463,7 @@ fixtures/
461
463
 
462
464
  ```bash
463
465
  gmax doctor # Check health
464
- gmax doctor --fix # Auto-repair (compact, prune, fix locks)
466
+ gmax doctor --fix # Repair checks; full-table maintenance is disabled
465
467
  gmax doctor --agent # Machine-readable health output
466
468
  gmax index # Reindex (auto-detects and repairs cache/vector mismatches)
467
469
  gmax index --reset # Full reindex from scratch
@@ -470,11 +472,11 @@ gmax watch restart # Restart daemon
470
472
 
471
473
  `gmax doctor` reports ANN index state. `ANN: vector index not built` is normal with the default exact-search configuration.
472
474
 
473
- Compaction pauses index writes, waits for pending writes to commit, and opens a fresh table snapshot for each attempt. Each optimize call makes at most two attempts. Before each attempt it checks current free space against twice the table's logical size plus the critical-space reserve (5 GB by default). If there is insufficient headroom, it skips the rewrite and logs the required space; search remains available. A successful prune can reclaim fragment copies left by failed attempts.
475
+ Full-table compaction and new FTS/ANN index builds are disabled in the containment release, including forced maintenance and doctor repair. Existing indexes remain readable; missing lexical indexes fall back to available retrieval. Retained copies are not recovered by this release. A separate exclusive prune-only path requires validation before deployment. Do not repeatedly force repair, use future cutoffs or manually remove index fragments.
474
476
 
475
- `~/.gmax/logs/daemon.log` records compaction attempts and outcomes, including duration, logical size, disk size before/after, free space, and bytes reclaimed. `gmax status --json` exposes the last outcome under `daemon.compaction`; its `at` timestamp identifies when it was recorded. Five-minute resource snapshots track footprint, RSS, heap, external/ArrayBuffer memory, Lance cache, workers and pending work for memory investigations. Reading status uses retained snapshots and does not scan the index directory. Logs rotate to `daemon.log.prev`.
477
+ `~/.gmax/logs/daemon.log` records containment skips and retained historical compaction outcomes, including duration, logical size, disk size before/after, free space, and bytes reclaimed. `gmax status --json` exposes the last outcome under `daemon.compaction`; its `at` timestamp identifies when it was recorded. Five-minute resource snapshots track footprint, RSS, heap, external/ArrayBuffer memory, Lance cache, workers and pending work for memory investigations. Reading status uses retained snapshots and does not scan the index directory. Logs rotate to `daemon.log.prev`.
476
478
 
477
- `gmax doctor --fix` reports completed, skipped, failed, or unverified optimization. It exits nonzero when a requested optimization did not complete or an older daemon could not verify its outcome. A busy or failed daemon is never retried as a second writer in the CLI process.
479
+ `gmax doctor --fix` refuses work under quarantine and otherwise reports optimization skipped by containment. It exits nonzero when a requested optimization did not complete or an older daemon could not verify its outcome. A busy or failed daemon is never retried as a second writer in the CLI process.
478
480
 
479
481
  ### Known issues
480
482
 
@@ -485,7 +487,7 @@ whether you need to act.
485
487
  | You see | What it means |
486
488
  |---|---|
487
489
  | `Optimize panicked ... inverted/builder.rs` | The historical FTS merge defect is fixed in the shipped LanceDB 0.39.0 runtime. If it recurs on a current release, preserve the version and logs and report a regression; the rebuild guard remains a recovery tripwire. |
488
- | `disabling auto-rebuild until an optimize succeeds` | Transient, not a wedge. The next successful optimize clears it. Search stays available. |
490
+ | `disabling auto-rebuild until an optimize succeeds` | Recovery remains paused under containment. Preserve diagnostics; use existing indexes or vector retrieval rather than forcing optimize. |
489
491
  | `ANN: vector index not built` | Normal — exact search is the default. |
490
492
  | `cannot reach the daemon socket from this sandbox` (exit 2) | The shell is sandboxed. Add the two keys in [Running under the Claude Code sandbox](#running-under-the-claude-code-sandbox); `gmax doctor` warns about the same gap. |
491
493
  | `npm audit` reports advisories on install | Record the installed version and dependency path and report new findings. JavaScript consumer audits do not cover the Python embedding environment; maintainers also audit its locked registry dependencies with `pnpm run audit:python` (Python 3.11+; no model load). |
@@ -53,6 +53,7 @@ const index_config_1 = require("../lib/index/index-config");
53
53
  const sync_helpers_1 = require("../lib/index/sync-helpers");
54
54
  const syncer_1 = require("../lib/index/syncer");
55
55
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
56
+ const maintenance_policy_1 = require("../lib/store/maintenance-policy");
56
57
  const vector_db_1 = require("../lib/store/vector-db");
57
58
  const autostart_1 = require("../lib/utils/autostart");
58
59
  const blocked_roots_1 = require("../lib/utils/blocked-roots");
@@ -87,6 +88,7 @@ Examples:
87
88
  var _a, _b, _c, _d, _e, _f;
88
89
  let vectorDb = null;
89
90
  try {
91
+ (0, maintenance_policy_1.assertStoreMutationAllowed)();
90
92
  const targetDir = dir ? path.resolve(dir) : process.cwd();
91
93
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(targetDir)) !== null && _a !== void 0 ? _a : targetDir;
92
94
  const projectName = path.basename(projectRoot);
@@ -51,6 +51,7 @@ const commander_1 = require("commander");
51
51
  const config_1 = require("../config");
52
52
  const embedding_status_1 = require("../lib/index/embedding-status");
53
53
  const index_config_1 = require("../lib/index/index-config");
54
+ const maintenance_policy_1 = require("../lib/store/maintenance-policy");
54
55
  const doctor_optimize_1 = require("../lib/utils/doctor-optimize");
55
56
  const doctor_status_1 = require("../lib/utils/doctor-status");
56
57
  const exit_1 = require("../lib/utils/exit");
@@ -346,6 +347,14 @@ exports.doctor = new commander_1.Command("doctor")
346
347
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
347
348
  var _a, _b;
348
349
  var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
350
+ if (opts.fix) {
351
+ const denied = (0, maintenance_policy_1.storeMutationDeniedReason)();
352
+ if (denied !== null) {
353
+ console.error(`gmax doctor --fix refused: ${denied}; preserving containment`);
354
+ process.exitCode = 2;
355
+ return;
356
+ }
357
+ }
349
358
  if (!opts.agent)
350
359
  console.log("gmax Doctor\n");
351
360
  const root = config_1.PATHS.globalRoot;
@@ -50,6 +50,7 @@ const grammar_loader_1 = require("../lib/index/grammar-loader");
50
50
  const sync_helpers_1 = require("../lib/index/sync-helpers");
51
51
  const syncer_1 = require("../lib/index/syncer");
52
52
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
53
+ const maintenance_policy_1 = require("../lib/store/maintenance-policy");
53
54
  const vector_db_1 = require("../lib/store/vector-db");
54
55
  const autostart_1 = require("../lib/utils/autostart");
55
56
  const exit_1 = require("../lib/utils/exit");
@@ -85,6 +86,7 @@ Examples:
85
86
  process.on("SIGINT", onSignal);
86
87
  process.on("SIGTERM", onSignal);
87
88
  try {
89
+ (0, maintenance_policy_1.assertStoreMutationAllowed)();
88
90
  yield (0, setup_helpers_1.ensureSetup)();
89
91
  const indexRoot = options.path
90
92
  ? path.resolve(options.path)
@@ -46,6 +46,7 @@ exports.remove = void 0;
46
46
  const path = __importStar(require("node:path"));
47
47
  const readline = __importStar(require("node:readline"));
48
48
  const commander_1 = require("commander");
49
+ const maintenance_policy_1 = require("../lib/store/maintenance-policy");
49
50
  const meta_cache_1 = require("../lib/store/meta-cache");
50
51
  const vector_db_1 = require("../lib/store/vector-db");
51
52
  const exit_1 = require("../lib/utils/exit");
@@ -82,6 +83,7 @@ Examples:
82
83
  let vectorDb = null;
83
84
  let metaCache = null;
84
85
  try {
86
+ (0, maintenance_policy_1.assertStoreMutationAllowed)();
85
87
  // Resolve name → registered root when arg has no path separator and
86
88
  // isn't a dir. Avoids the footgun where a typo'd name silently removed
87
89
  // the cwd project.
@@ -13,6 +13,7 @@ exports.repair = void 0;
13
13
  const commander_1 = require("commander");
14
14
  const config_1 = require("../config");
15
15
  const index_config_1 = require("../lib/index/index-config");
16
+ const maintenance_policy_1 = require("../lib/store/maintenance-policy");
16
17
  const vector_db_1 = require("../lib/store/vector-db");
17
18
  const autostart_1 = require("../lib/utils/autostart");
18
19
  const daemon_client_1 = require("../lib/utils/daemon-client");
@@ -38,6 +39,7 @@ Examples:
38
39
  var _a, _b, _c;
39
40
  try {
40
41
  if (opts.rebuild) {
42
+ (0, maintenance_policy_1.assertStoreMutationAllowed)();
41
43
  const running = yield (0, daemon_client_1.ensureDaemonRunning)();
42
44
  if (!running) {
43
45
  const undo = (0, autostart_1.autostartDisabledUndo)();
@@ -76,6 +76,12 @@ exports.watch = new commander_1.Command("watch")
76
76
  .option("--no-idle-timeout", "Disable the 30-minute idle shutdown")
77
77
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
78
78
  var _a, _b;
79
+ const safetyDenied = (0, autostart_1.daemonStartDeniedReason)();
80
+ if (safetyDenied !== null) {
81
+ console.error(`gmax: ${safetyDenied}; preserving containment`);
82
+ process.exitCode = 2;
83
+ return;
84
+ }
79
85
  // A secondary store (src/bin.ts) never runs a daemon or a watcher: one
80
86
  // would hold the external drive open, and its startup sweep of stale
81
87
  // daemons judges liveness by its own home's lock and socket, so it takes
@@ -440,6 +446,12 @@ exports.watch
440
446
  error,
441
447
  });
442
448
  };
449
+ const deniedStartup = (0, autostart_1.daemonStartDeniedReason)();
450
+ if (deniedStartup !== null) {
451
+ refuse(`${deniedStartup}; preserving containment`);
452
+ yield (0, exit_1.gracefulExit)(2);
453
+ return;
454
+ }
443
455
  // Refused before anything is stopped: a daemon that cannot be started
444
456
  // again from here must not be taken down.
445
457
  if (process.env.GMAX_SECONDARY_STORE === "1") {
@@ -60,6 +60,7 @@ const server_1 = require("../llm/server");
60
60
  const meta_cache_1 = require("../store/meta-cache");
61
61
  const store_lease_1 = require("../store/store-lease");
62
62
  const vector_db_1 = require("../store/vector-db");
63
+ const autostart_1 = require("../utils/autostart");
63
64
  const blocked_roots_1 = require("../utils/blocked-roots");
64
65
  const daemon_client_1 = require("../utils/daemon-client");
65
66
  const daemon_launcher_1 = require("../utils/daemon-launcher");
@@ -73,6 +74,7 @@ const process_footprint_1 = require("../utils/process-footprint");
73
74
  const project_registry_1 = require("../utils/project-registry");
74
75
  const resource_snapshot_1 = require("../utils/resource-snapshot");
75
76
  const root_availability_1 = require("../utils/root-availability");
77
+ const safety_latch_1 = require("../utils/safety-latch");
76
78
  const watcher_store_1 = require("../utils/watcher-store");
77
79
  const pool_1 = require("../workers/pool");
78
80
  const ipc_handler_1 = require("./ipc-handler");
@@ -148,8 +150,6 @@ class Daemon {
148
150
  this.heartbeatInterval = null;
149
151
  this.idleInterval = null;
150
152
  this.heartbeatTick = 0;
151
- /** Rate-limits the kernel-zone warn log — see checkKernelZonePressure. */
152
- this.lastZoneWarnMs = 0;
153
153
  this.shuttingDown = false;
154
154
  this.recycling = false;
155
155
  this.recycleDueSinceMs = null;
@@ -238,6 +238,19 @@ class Daemon {
238
238
  start() {
239
239
  return __awaiter(this, void 0, void 0, function* () {
240
240
  var _a;
241
+ const denied = (0, autostart_1.daemonStartDeniedReason)();
242
+ if (denied !== null)
243
+ throw new Error(`gmax: ${denied}; preserving containment`);
244
+ // Check before stale-process cleanup, store opening, workers or model setup.
245
+ if (process.platform === "darwin") {
246
+ const usage = (0, kernel_zone_1.readKernelZoneUsage)();
247
+ if (!usage)
248
+ throw new Error("gmax: kernel pressure unavailable; startup refused");
249
+ if (usage.pressure !== "ok") {
250
+ (0, safety_latch_1.latchSafetyStop)((0, kernel_zone_1.formatZoneUsage)(usage));
251
+ throw new Error(`gmax: ${(0, kernel_zone_1.formatZoneUsage)(usage)}; startup safety stop`);
252
+ }
253
+ }
241
254
  process.title = "gmax-daemon";
242
255
  // Read verbs register here rather than at read-verbs.ts module scope: the
243
256
  // registry is asserted on exactly in tests/ipc-read-verbs.test.ts, and a
@@ -457,13 +470,10 @@ class Daemon {
457
470
  // after a frozen MLX process kept the port bound (v0.17.0 bug #1).
458
471
  this.heartbeatTick++;
459
472
  if (this.heartbeatTick % 5 === 0) {
460
- void this.mlxServerManager.checkMlxHealth();
461
- this.processManager.sweepOrphanWorkers();
462
- this.maybeRecycle();
463
- this.checkKernelZonePressure();
473
+ this.runHeartbeatMaintenance(true);
464
474
  }
465
475
  else if (this.recycleDueSinceMs !== null) {
466
- this.maybeRecycle(false);
476
+ this.runHeartbeatMaintenance(false);
467
477
  }
468
478
  }, HEARTBEAT_INTERVAL_MS);
469
479
  // 10. Idle timeout (skip when disabled via env)
@@ -606,7 +616,22 @@ class Daemon {
606
616
  }
607
617
  }
608
618
  runSharedOperation(name, signal, fn) {
609
- return this.operations.runShared(name, signal, fn);
619
+ const readOnly = (0, read_verbs_1.getReadVerb)(name) !== undefined ||
620
+ ["search", "project-stats", "unwatch", "llm-stop"].includes(name);
621
+ return this.operations.runShared(name, signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
622
+ if (!readOnly)
623
+ this.assertHeavyOperationAdmission(name);
624
+ return fn(operationSignal);
625
+ }));
626
+ }
627
+ assertHeavyOperationAdmission(name) {
628
+ const denied = (0, autostart_1.daemonStartDeniedReason)();
629
+ if (denied !== null)
630
+ throw new Error(`${name} refused: ${denied}`);
631
+ // Probe at operation admission, before setup/cache/worker/model side effects.
632
+ if (!this.checkKernelZonePressure()) {
633
+ throw new Error(`${name} refused: host pressure is unsafe or unknown`);
634
+ }
610
635
  }
611
636
  /**
612
637
  * Compact + prune on behalf of a CLI caller.
@@ -1298,6 +1323,18 @@ class Daemon {
1298
1323
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
1299
1324
  return;
1300
1325
  }
1326
+ try {
1327
+ this.assertHeavyOperationAdmission("repair");
1328
+ }
1329
+ catch (error) {
1330
+ (0, ipc_handler_1.writeDone)(conn, {
1331
+ ok: false,
1332
+ error: error instanceof Error
1333
+ ? error.message
1334
+ : "repair safety admission failed",
1335
+ });
1336
+ return;
1337
+ }
1301
1338
  const targetConfig = (0, index_config_1.readGlobalConfig)();
1302
1339
  const targetGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(targetConfig);
1303
1340
  const clientAbort = new AbortController();
@@ -1338,8 +1375,10 @@ class Daemon {
1338
1375
  };
1339
1376
  try {
1340
1377
  const result = yield this.operations.runExclusive("repair", () => __awaiter(this, void 0, void 0, function* () {
1378
+ this.assertHeavyOperationAdmission("repair");
1341
1379
  desiredWatchRoots = yield this.watcherManager.quiesceAll();
1342
1380
  }), (operationSignal) => __awaiter(this, void 0, void 0, function* () {
1381
+ this.assertHeavyOperationAdmission("repair");
1343
1382
  throwIfPreDropCancelled(operationSignal);
1344
1383
  oldResources.vectorDb.pauseMaintenanceLoop();
1345
1384
  this.searchers.clear();
@@ -1843,6 +1882,16 @@ class Daemon {
1843
1882
  resourceSnapshot() {
1844
1883
  return this.lastResourceSnapshot;
1845
1884
  }
1885
+ runHeartbeatMaintenance(sampleFootprint) {
1886
+ // Both ordinary probes and deferred-recycle ticks check safety first.
1887
+ if (!this.checkKernelZonePressure())
1888
+ return;
1889
+ this.maybeRecycle(sampleFootprint);
1890
+ if (this.shuttingDown || this.recycling || !sampleFootprint)
1891
+ return;
1892
+ void this.mlxServerManager.checkMlxHealth();
1893
+ this.processManager.sweepOrphanWorkers();
1894
+ }
1846
1895
  logResourceSnapshot(reason, footprintMb) {
1847
1896
  var _a, _b;
1848
1897
  var _c, _d;
@@ -1941,38 +1990,45 @@ class Daemon {
1941
1990
  * This host lost three sessions that way before the compactor was identified as
1942
1991
  * the largest write source on it.
1943
1992
  *
1944
- * So the daemon watches the zone it is pressurizing. A warn-level reading is
1945
- * logged and left alone; a critical reading exits without relaunch, because
1946
- * relaunching would resume the writes that caused it. Nothing here can reclaim
1947
- * the leaked memory — only a reboot does that — so the only useful move is to
1948
- * stop adding to it while the user still has a working machine.
1993
+ * Warning, critical or unknown macOS readings stop mutation before another
1994
+ * heavy operation can begin. Persist the reason before shutdown, so hooks and
1995
+ * memory guards cannot revive the writer. Nothing here reclaims kernel memory.
1949
1996
  *
1950
- * Deliberately does nothing when the sample is unavailable or unparseable
1951
- * (non-macOS, no `zprint`, changed output format): an unknown reading must never
1952
- * shut the daemon down.
1997
+ * Non-macOS hosts have no such probe. On macOS an unavailable sample is
1998
+ * explicitly unknown and cannot authorize further mutation.
1953
1999
  */
1954
2000
  checkKernelZonePressure() {
1955
2001
  if (this.shuttingDown || this.recycling)
1956
- return;
2002
+ return false;
2003
+ const denied = (0, autostart_1.daemonStartDeniedReason)();
2004
+ if (denied !== null) {
2005
+ // The existing quarantine is already durable; preserve its ownership and
2006
+ // do not replace it with a new marker that an operator must also clear.
2007
+ this.stopForSafety(denied, false);
2008
+ return false;
2009
+ }
2010
+ if (process.platform !== "darwin")
2011
+ return true;
1957
2012
  const usage = (0, kernel_zone_1.readKernelZoneUsage)();
1958
- if (!usage || usage.pressure === "ok")
1959
- return;
1960
- if (usage.pressure === "warn") {
1961
- // Rate-limited to once an hour: at the 5-minute heartbeat this would
1962
- // otherwise print 12 identical lines an hour for as long as the condition
1963
- // holds, which on a leaking host is until reboot.
1964
- const now = Date.now();
1965
- if (now - this.lastZoneWarnMs < 60 * 60 * 1000)
1966
- return;
1967
- this.lastZoneWarnMs = now;
1968
- console.log(`[daemon] WARNING: ${(0, kernel_zone_1.formatZoneUsage)(usage)} — macOS kernel zone is leaking ` +
1969
- `under write pressure. The daemon will stop itself at ` +
1970
- `${(kernel_zone_1.ZONE_THRESHOLDS.criticalBytes / Math.pow(1024, 3)).toFixed(0)}GiB. ` +
1971
- `Only a reboot reclaims this memory.`);
1972
- return;
2013
+ if ((usage === null || usage === void 0 ? void 0 : usage.pressure) === "ok")
2014
+ return true;
2015
+ const reason = usage
2016
+ ? `${usage.pressure} kernel pressure: ${(0, kernel_zone_1.formatZoneUsage)(usage)}`
2017
+ : "kernel pressure unavailable; heavy work cannot be admitted";
2018
+ this.stopForSafety(reason);
2019
+ return false;
2020
+ }
2021
+ stopForSafety(reason, persist = true) {
2022
+ // Warning already pauses mutation: a stale index costs less than another
2023
+ // host crash. Unknown readings are visible stops, never healthy samples.
2024
+ try {
2025
+ if (persist)
2026
+ (0, safety_latch_1.latchSafetyStop)(reason);
2027
+ }
2028
+ catch (error) {
2029
+ console.error("[daemon] Failed to persist safety stop; stopping anyway:", error);
1973
2030
  }
1974
- console.log(`[daemon] CRITICAL: ${(0, kernel_zone_1.formatZoneUsage)(usage)} — stopping to avoid a kernel panic. ` +
1975
- `Reboot to reclaim the zone; the index will catch up on next start.`);
2031
+ console.log(`[daemon] SAFETY STOP: ${reason}. Containment stays in place across restart; review host health before resuming.`);
1976
2032
  this.recycling = true;
1977
2033
  void this.shutdown().finally(() => process.exit(0));
1978
2034
  }
@@ -47,6 +47,9 @@ exports.compactionSchema = zod_1.z.object({
47
47
  freeBytesBefore: zod_1.z.number().optional(),
48
48
  freeBytesAfter: zod_1.z.number().optional(),
49
49
  bytesReclaimed: zod_1.z.number().optional(),
50
+ netBytesReclaimed: zod_1.z.number().optional(),
51
+ cleanupPasses: zod_1.z.number().int().min(0).max(1).optional(),
52
+ cleanupReason: zod_1.z.string().optional(),
50
53
  });
51
54
  const embedding = zod_1.z.object({
52
55
  tier: zod_1.z.string(),
@@ -0,0 +1,172 @@
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.DiskPressureError = exports.FULL_TABLE_MAINTENANCE_DISABLED_REASON = void 0;
37
+ exports.fullTableMaintenanceDisabled = fullTableMaintenanceDisabled;
38
+ exports.availableStoreDiskBytes = availableStoreDiskBytes;
39
+ exports.assertFreshDiskMutationAllowed = assertFreshDiskMutationAllowed;
40
+ exports.storeMutationDeniedReason = storeMutationDeniedReason;
41
+ exports.assertStoreMutationAllowed = assertStoreMutationAllowed;
42
+ exports.maintenancePolicyPath = maintenancePolicyPath;
43
+ exports.recordMaintenanceContainment = recordMaintenanceContainment;
44
+ const node_crypto_1 = require("node:crypto");
45
+ const fs = __importStar(require("node:fs"));
46
+ const path = __importStar(require("node:path"));
47
+ const config_1 = require("../../config");
48
+ const autostart_1 = require("../utils/autostart");
49
+ const safety_latch_1 = require("../utils/safety-latch");
50
+ exports.FULL_TABLE_MAINTENANCE_DISABLED_REASON = "full-table maintenance disabled by host-safety containment; disk recovery is pending";
51
+ /** This release has no production override: force, configuration and persisted
52
+ * state cannot enable rewriting. Tests may mock this module to cover the old
53
+ * native algorithm without turning it into a supported production entry point. */
54
+ function fullTableMaintenanceDisabled() {
55
+ return true;
56
+ }
57
+ class DiskPressureError extends Error {
58
+ constructor(message = "Disk critically low or unknown — writes suspended") {
59
+ super(message);
60
+ this.name = "DiskPressureError";
61
+ }
62
+ }
63
+ exports.DiskPressureError = DiskPressureError;
64
+ /** Fresh, bounded filesystem sample. ENOENT alone permits trying an ancestor;
65
+ * permission errors, invalid values and all other uncertainty refuse writes. */
66
+ function availableStoreDiskBytes(storeDir) {
67
+ let existing = path.resolve(storeDir);
68
+ for (;;) {
69
+ try {
70
+ const stats = fs.statfsSync(existing);
71
+ const available = stats.bavail * stats.bsize;
72
+ return Number.isFinite(available) && available >= 0
73
+ ? available
74
+ : Number.NaN;
75
+ }
76
+ catch (error) {
77
+ const parent = path.dirname(existing);
78
+ if (error.code !== "ENOENT" ||
79
+ parent === existing)
80
+ return Number.NaN;
81
+ existing = parent;
82
+ }
83
+ }
84
+ }
85
+ /** Reusable admission for LMDB and other store mutations. Never cached. */
86
+ function assertFreshDiskMutationAllowed(storeDir) {
87
+ const available = availableStoreDiskBytes(storeDir);
88
+ if (!Number.isFinite(available) || available < config_1.DISK_CRITICAL_BYTES)
89
+ throw new DiskPressureError();
90
+ return available;
91
+ }
92
+ function storeMutationDeniedReason() {
93
+ const safety = (0, safety_latch_1.safetyStopReason)();
94
+ if (safety !== null)
95
+ return `host safety stop: ${safety}`;
96
+ return (0, autostart_1.autostartDisabledReason)() !== null
97
+ ? "store mutations are quarantined while daemon autostart is disabled"
98
+ : null;
99
+ }
100
+ function assertStoreMutationAllowed() {
101
+ const reason = storeMutationDeniedReason();
102
+ if (reason !== null)
103
+ throw new Error(`gmax store mutation blocked: ${reason}`);
104
+ }
105
+ function maintenancePolicyPath(storeDir) {
106
+ return path.join(storeDir, ".gmax-maintenance-policy.json");
107
+ }
108
+ /** Persist a zero-rewrite receipt once. Unknown/corrupt/unwritable state never
109
+ * weakens the immutable default. No timestamps or budgets authorize native work. */
110
+ function recordMaintenanceContainment(storeDir) {
111
+ let temporary;
112
+ try {
113
+ fs.mkdirSync(storeDir, { recursive: true, mode: 0o700 });
114
+ const storeIdentity = fs.realpathSync(storeDir);
115
+ const target = maintenancePolicyPath(storeIdentity);
116
+ if (fs.existsSync(target)) {
117
+ const stat = fs.lstatSync(target);
118
+ if (!stat.isFile() || stat.size > 16 * 1024)
119
+ throw new Error("unsafe maintenance policy file");
120
+ const previous = JSON.parse(fs.readFileSync(target, "utf8"));
121
+ if (previous.schemaVersion === 1 &&
122
+ previous.storeIdentity === storeIdentity &&
123
+ previous.mode === "disabled" &&
124
+ previous.rewriteBudgetBytes === 0 &&
125
+ previous.cleanupPending === true) {
126
+ fs.chmodSync(target, 0o600);
127
+ return exports.FULL_TABLE_MAINTENANCE_DISABLED_REASON;
128
+ }
129
+ }
130
+ const policy = {
131
+ schemaVersion: 1,
132
+ storeIdentity,
133
+ mode: "disabled",
134
+ reason: exports.FULL_TABLE_MAINTENANCE_DISABLED_REASON,
135
+ recordedAt: Date.now(),
136
+ rewriteBudgetBytes: 0,
137
+ cleanupPending: true,
138
+ };
139
+ temporary = path.join(storeIdentity, `.gmax-maintenance-${(0, node_crypto_1.randomUUID)()}.tmp`);
140
+ const descriptor = fs.openSync(temporary, "wx", 0o600);
141
+ try {
142
+ fs.writeFileSync(descriptor, `${JSON.stringify(policy)}\n`);
143
+ fs.fsyncSync(descriptor);
144
+ }
145
+ finally {
146
+ fs.closeSync(descriptor);
147
+ }
148
+ fs.renameSync(temporary, target);
149
+ temporary = undefined;
150
+ const directory = fs.openSync(storeIdentity, "r");
151
+ try {
152
+ fs.fsyncSync(directory);
153
+ }
154
+ finally {
155
+ fs.closeSync(directory);
156
+ }
157
+ return exports.FULL_TABLE_MAINTENANCE_DISABLED_REASON;
158
+ }
159
+ catch (_a) {
160
+ return `${exports.FULL_TABLE_MAINTENANCE_DISABLED_REASON}; persistent policy unavailable or invalid (still disabled)`;
161
+ }
162
+ finally {
163
+ if (temporary) {
164
+ try {
165
+ fs.unlinkSync(temporary);
166
+ }
167
+ catch (_b) {
168
+ // Failed receipt cleanup cannot authorize maintenance.
169
+ }
170
+ }
171
+ }
172
+ }
@@ -67,14 +67,39 @@ const fs = __importStar(require("node:fs"));
67
67
  const path = __importStar(require("node:path"));
68
68
  const lmdb_1 = require("lmdb");
69
69
  const cleanup_1 = require("../utils/cleanup");
70
+ const maintenance_policy_1 = require("./maintenance-policy");
70
71
  class MetaCache {
71
72
  constructor(lmdbPath) {
73
+ this.lmdbPath = lmdbPath;
72
74
  this.closed = false;
73
- fs.mkdirSync(path.dirname(lmdbPath), { recursive: true });
75
+ let diskAllowsWrites = false;
76
+ try {
77
+ (0, maintenance_policy_1.assertFreshDiskMutationAllowed)(lmdbPath);
78
+ diskAllowsWrites = true;
79
+ }
80
+ catch (_a) {
81
+ /* Unknown/critical disk allows existing-cache reads only. */
82
+ }
83
+ this.readOnly = (0, maintenance_policy_1.storeMutationDeniedReason)() !== null || !diskAllowsWrites;
84
+ if (this.readOnly) {
85
+ // Quarantine permits reads of an existing cache, never creating one.
86
+ try {
87
+ fs.lstatSync(lmdbPath);
88
+ }
89
+ catch (_b) {
90
+ throw new Error("gmax metadata cache is quarantined or disk-constrained and unavailable");
91
+ }
92
+ }
93
+ else {
94
+ (0, maintenance_policy_1.assertStoreMutationAllowed)();
95
+ (0, maintenance_policy_1.assertFreshDiskMutationAllowed)(lmdbPath);
96
+ fs.mkdirSync(path.dirname(lmdbPath), { recursive: true });
97
+ }
74
98
  this.db = (0, lmdb_1.open)({
75
99
  path: lmdbPath,
76
100
  compression: true,
77
101
  cache: true,
102
+ readOnly: this.readOnly,
78
103
  });
79
104
  this.unregisterCleanup = (0, cleanup_1.registerCleanup)(() => this.close());
80
105
  }
@@ -129,11 +154,19 @@ class MetaCache {
129
154
  });
130
155
  }
131
156
  put(filePath, entry) {
157
+ this.assertWritable();
132
158
  this.db.put(filePath, entry);
133
159
  }
134
160
  delete(filePath) {
161
+ this.assertWritable();
135
162
  this.db.remove(filePath);
136
163
  }
164
+ assertWritable() {
165
+ (0, maintenance_policy_1.assertStoreMutationAllowed)();
166
+ (0, maintenance_policy_1.assertFreshDiskMutationAllowed)(this.lmdbPath);
167
+ if (this.readOnly)
168
+ throw new Error("gmax metadata cache was opened read-only; reopen after reviewing containment");
169
+ }
137
170
  entries() {
138
171
  return __asyncGenerator(this, arguments, function* entries_1() {
139
172
  var _a, e_3, _b, _c;