grepmax 0.26.54 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +9 -7
- package/dist/commands/add.js +2 -0
- package/dist/commands/doctor.js +9 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/remove.js +2 -0
- package/dist/commands/repair.js +2 -0
- package/dist/commands/watch.js +12 -0
- package/dist/lib/daemon/daemon.js +90 -34
- package/dist/lib/store/maintenance-policy.js +172 -0
- package/dist/lib/store/meta-cache.js +34 -1
- package/dist/lib/store/vector-db.js +72 -29
- package/dist/lib/utils/autostart.js +34 -7
- package/dist/lib/utils/daemon-launcher.js +6 -0
- package/dist/lib/utils/kernel-zone.js +10 -5
- package/dist/lib/utils/safety-latch.js +121 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/watch-lease.js +13 -5
|
@@ -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.
|
|
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 #
|
|
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;
|
|
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 #
|
|
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
|
-
|
|
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
|
|
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`
|
|
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` |
|
|
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). |
|
package/dist/commands/add.js
CHANGED
|
@@ -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);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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;
|
package/dist/commands/index.js
CHANGED
|
@@ -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)
|
package/dist/commands/remove.js
CHANGED
|
@@ -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.
|
package/dist/commands/repair.js
CHANGED
|
@@ -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)();
|
package/dist/commands/watch.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
1945
|
-
*
|
|
1946
|
-
*
|
|
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
|
-
*
|
|
1951
|
-
*
|
|
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 (
|
|
1959
|
-
return;
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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]
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -66,6 +66,7 @@ const query_timeout_1 = require("../utils/query-timeout");
|
|
|
66
66
|
const ann_config_1 = require("./ann-config");
|
|
67
67
|
const compaction_result_1 = require("./compaction-result");
|
|
68
68
|
const lancedb = __importStar(require("./lance-sdk"));
|
|
69
|
+
const maintenance_policy_1 = require("./maintenance-policy");
|
|
69
70
|
const store_lease_1 = require("./store-lease");
|
|
70
71
|
/**
|
|
71
72
|
* Find abandoned Lance scratch files under `<lancedbDir>/ *.lance/data`.
|
|
@@ -134,13 +135,8 @@ function sweepStaleLanceTempFiles(lancedbDir, minAgeMs = config_1.STALE_TEMP_FIL
|
|
|
134
135
|
}
|
|
135
136
|
return { filesRemoved, bytesFreed };
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
super(message);
|
|
140
|
-
this.name = "DiskPressureError";
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
exports.DiskPressureError = DiskPressureError;
|
|
138
|
+
var maintenance_policy_2 = require("./maintenance-policy");
|
|
139
|
+
Object.defineProperty(exports, "DiskPressureError", { enumerable: true, get: function () { return maintenance_policy_2.DiskPressureError; } });
|
|
144
140
|
/**
|
|
145
141
|
* Detects "Not found: <hash>.lance" errors from LanceDB — these indicate the
|
|
146
142
|
* manifest references a fragment file that doesn't exist on disk, typically
|
|
@@ -370,11 +366,20 @@ class VectorDB {
|
|
|
370
366
|
if (this.closed) {
|
|
371
367
|
throw new Error("VectorDB connection is closed");
|
|
372
368
|
}
|
|
369
|
+
const creatingStore = !fs.existsSync(this.lancedbDir);
|
|
370
|
+
if (creatingStore) {
|
|
371
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
372
|
+
(0, maintenance_policy_1.assertFreshDiskMutationAllowed)(this.lancedbDir);
|
|
373
|
+
}
|
|
373
374
|
yield this.getLease();
|
|
374
375
|
if (this.closed) {
|
|
375
376
|
throw new Error("VectorDB connection is closed");
|
|
376
377
|
}
|
|
377
378
|
if (!this.db) {
|
|
379
|
+
if (creatingStore || !fs.existsSync(this.lancedbDir)) {
|
|
380
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
381
|
+
(0, maintenance_policy_1.assertFreshDiskMutationAllowed)(this.lancedbDir);
|
|
382
|
+
}
|
|
378
383
|
fs.mkdirSync(this.lancedbDir, { recursive: true });
|
|
379
384
|
this.session = createLanceSession();
|
|
380
385
|
// 0.38 accepts the legacy third argument in its types but drops it in
|
|
@@ -469,23 +474,18 @@ class VectorDB {
|
|
|
469
474
|
});
|
|
470
475
|
}
|
|
471
476
|
getAvailableBytes() {
|
|
472
|
-
|
|
473
|
-
const stats = fs.statfsSync(this.lancedbDir);
|
|
474
|
-
return stats.bavail * stats.bsize;
|
|
475
|
-
}
|
|
476
|
-
catch (_a) {
|
|
477
|
-
return Number.MAX_SAFE_INTEGER; // fail-open
|
|
478
|
-
}
|
|
477
|
+
return (0, maintenance_policy_1.availableStoreDiskBytes)(this.lancedbDir);
|
|
479
478
|
}
|
|
480
|
-
checkDiskPressure() {
|
|
479
|
+
checkDiskPressure(fresh = false) {
|
|
481
480
|
const now = Date.now();
|
|
482
|
-
if (
|
|
481
|
+
if (!fresh &&
|
|
482
|
+
now - this.lastDiskCheckMs < VectorDB.DISK_CHECK_INTERVAL_MS) {
|
|
483
483
|
return this.diskPressure;
|
|
484
484
|
}
|
|
485
485
|
this.lastDiskCheckMs = now;
|
|
486
486
|
const avail = this.getAvailableBytes();
|
|
487
487
|
let level;
|
|
488
|
-
if (avail < config_1.DISK_CRITICAL_BYTES) {
|
|
488
|
+
if (!Number.isFinite(avail) || avail < 0 || avail < config_1.DISK_CRITICAL_BYTES) {
|
|
489
489
|
level = "critical";
|
|
490
490
|
}
|
|
491
491
|
else if (avail < config_1.DISK_LOW_BYTES) {
|
|
@@ -497,7 +497,7 @@ class VectorDB {
|
|
|
497
497
|
if (level !== this.lastLoggedPressure) {
|
|
498
498
|
const freeStr = `${(avail / 1024 / 1024 / 1024).toFixed(1)}GB`;
|
|
499
499
|
if (level === "critical") {
|
|
500
|
-
(0, logger_1.log)("vectordb", `CRITICAL: disk space critically low (${freeStr} free) — writes suspended`);
|
|
500
|
+
(0, logger_1.log)("vectordb", `CRITICAL: ${Number.isFinite(avail) ? `disk space critically low (${freeStr} free)` : "disk space unknown"} — writes suspended`);
|
|
501
501
|
}
|
|
502
502
|
else if (level === "low") {
|
|
503
503
|
(0, logger_1.log)("vectordb", `WARNING: disk space low (${freeStr} free) — compaction limited`);
|
|
@@ -511,8 +511,8 @@ class VectorDB {
|
|
|
511
511
|
return level;
|
|
512
512
|
}
|
|
513
513
|
ensureDiskOk() {
|
|
514
|
-
if (this.checkDiskPressure() === "critical") {
|
|
515
|
-
throw new DiskPressureError();
|
|
514
|
+
if (this.checkDiskPressure(true) === "critical") {
|
|
515
|
+
throw new maintenance_policy_1.DiskPressureError();
|
|
516
516
|
}
|
|
517
517
|
}
|
|
518
518
|
/**
|
|
@@ -522,6 +522,8 @@ class VectorDB {
|
|
|
522
522
|
*/
|
|
523
523
|
withWriteGate(fn) {
|
|
524
524
|
return __awaiter(this, void 0, void 0, function* () {
|
|
525
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
526
|
+
this.ensureDiskOk();
|
|
525
527
|
while (this.exclusiveMutationPromise || this.compactingPromise) {
|
|
526
528
|
if (this.closed)
|
|
527
529
|
throw new Error("VectorDB connection is closed");
|
|
@@ -529,6 +531,8 @@ class VectorDB {
|
|
|
529
531
|
}
|
|
530
532
|
if (this.closed)
|
|
531
533
|
throw new Error("VectorDB connection is closed");
|
|
534
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
535
|
+
this.ensureDiskOk();
|
|
532
536
|
this.activeWrites++;
|
|
533
537
|
try {
|
|
534
538
|
return yield fn();
|
|
@@ -567,6 +571,8 @@ class VectorDB {
|
|
|
567
571
|
}
|
|
568
572
|
withExclusiveTableMutation(mutation) {
|
|
569
573
|
return __awaiter(this, void 0, void 0, function* () {
|
|
574
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
575
|
+
this.ensureDiskOk();
|
|
570
576
|
if (this.closed)
|
|
571
577
|
throw new Error("VectorDB connection is closed");
|
|
572
578
|
if (this.exclusiveMutationPromise) {
|
|
@@ -592,6 +598,8 @@ class VectorDB {
|
|
|
592
598
|
});
|
|
593
599
|
}
|
|
594
600
|
yield Promise.all([this.drainWrites(), this.drainCompactions()]);
|
|
601
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
602
|
+
this.ensureDiskOk();
|
|
595
603
|
const db = yield this.getDb();
|
|
596
604
|
const result = yield mutation(db);
|
|
597
605
|
this.markWriteCommitted();
|
|
@@ -761,6 +769,14 @@ class VectorDB {
|
|
|
761
769
|
}
|
|
762
770
|
ensureTable() {
|
|
763
771
|
return __awaiter(this, void 0, void 0, function* () {
|
|
772
|
+
if ((0, maintenance_policy_1.storeMutationDeniedReason)() !== null ||
|
|
773
|
+
this.checkDiskPressure(true) === "critical") {
|
|
774
|
+
const existing = yield this.openExistingTableUnsafe();
|
|
775
|
+
if (!existing)
|
|
776
|
+
throw new Error("gmax store is quarantined and has no existing table");
|
|
777
|
+
yield this.validateSchema(existing);
|
|
778
|
+
return existing;
|
|
779
|
+
}
|
|
764
780
|
return this.withWriteGate(() => this.ensureTableUnsafe());
|
|
765
781
|
});
|
|
766
782
|
}
|
|
@@ -774,6 +790,7 @@ class VectorDB {
|
|
|
774
790
|
catch (err) {
|
|
775
791
|
if (!isMissingTableError(err))
|
|
776
792
|
throw err;
|
|
793
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
777
794
|
(0, logger_1.log)("db", `Creating table (${this.vectorDim}d)`);
|
|
778
795
|
const schema = this.buildSchema();
|
|
779
796
|
table = yield db.createTable(TABLE_NAME, [this.seedRow()], {
|
|
@@ -784,6 +801,7 @@ class VectorDB {
|
|
|
784
801
|
return table;
|
|
785
802
|
}
|
|
786
803
|
yield this.validateSchema(table);
|
|
804
|
+
(0, maintenance_policy_1.assertStoreMutationAllowed)();
|
|
787
805
|
yield this.evolveSchema(table);
|
|
788
806
|
return table;
|
|
789
807
|
});
|
|
@@ -903,6 +921,12 @@ class VectorDB {
|
|
|
903
921
|
}
|
|
904
922
|
createFTSIndex() {
|
|
905
923
|
return __awaiter(this, arguments, void 0, function* (rebuild = false, retries = 5) {
|
|
924
|
+
if ((0, maintenance_policy_1.fullTableMaintenanceDisabled)()) {
|
|
925
|
+
if (rebuild)
|
|
926
|
+
throw new Error(maintenance_policy_1.FULL_TABLE_MAINTENANCE_DISABLED_REASON);
|
|
927
|
+
// Existing indexes remain searchable; absence does not authorize a build.
|
|
928
|
+
return this.adoptFTSIndex();
|
|
929
|
+
}
|
|
906
930
|
return this.withWriteGate(() => this.createFTSIndexUnsafe(rebuild, retries));
|
|
907
931
|
});
|
|
908
932
|
}
|
|
@@ -939,7 +963,9 @@ class VectorDB {
|
|
|
939
963
|
return __awaiter(this, void 0, void 0, function* () {
|
|
940
964
|
if (this.ftsIndexEnsured)
|
|
941
965
|
return;
|
|
942
|
-
const table = yield this.
|
|
966
|
+
const table = yield this.openExistingTableUnsafe();
|
|
967
|
+
if (!table)
|
|
968
|
+
throw new Error("[vector-db] No existing table or FTS index; index maintenance is disabled");
|
|
943
969
|
const indices = yield table.listIndices();
|
|
944
970
|
const existing = indices.find((index) => index.name === "content_idx" || index.columns.includes("content"));
|
|
945
971
|
if (!existing) {
|
|
@@ -950,6 +976,10 @@ class VectorDB {
|
|
|
950
976
|
}
|
|
951
977
|
createVectorIndex() {
|
|
952
978
|
return __awaiter(this, arguments, void 0, function* (rebuild = false, retries = 5, checkOnly = false) {
|
|
979
|
+
if ((0, maintenance_policy_1.fullTableMaintenanceDisabled)()) {
|
|
980
|
+
(0, logger_1.debug)("vectordb", "ANN/index maintenance disabled by host-safety containment");
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
953
983
|
if (this.checkDiskPressure() !== "ok") {
|
|
954
984
|
(0, logger_1.debug)("vectordb", "ANN index skipped under disk pressure");
|
|
955
985
|
return false;
|
|
@@ -1182,6 +1212,10 @@ class VectorDB {
|
|
|
1182
1212
|
return __awaiter(this, arguments, void 0, function* (retries = COMPACTION_MAX_ATTEMPTS, retentionMs = 0, bypassExclusiveMutation = false) {
|
|
1183
1213
|
var _a;
|
|
1184
1214
|
var _b;
|
|
1215
|
+
if ((0, maintenance_policy_1.fullTableMaintenanceDisabled)()) {
|
|
1216
|
+
this.lastOptimizeDidWork = false;
|
|
1217
|
+
return this.recordSkippedCompaction((0, maintenance_policy_1.recordMaintenanceContainment)(this.lancedbDir));
|
|
1218
|
+
}
|
|
1185
1219
|
if (!bypassExclusiveMutation) {
|
|
1186
1220
|
while (this.exclusiveMutationPromise)
|
|
1187
1221
|
yield this.exclusiveMutationPromise;
|
|
@@ -1372,10 +1406,14 @@ class VectorDB {
|
|
|
1372
1406
|
/**
|
|
1373
1407
|
* Run FTS rebuild + optimize as a single serialized operation.
|
|
1374
1408
|
* Safe to call from multiple project processors — only one runs at a time.
|
|
1375
|
-
*
|
|
1409
|
+
* Reports bloat without another whole-table rewrite.
|
|
1376
1410
|
*/
|
|
1377
1411
|
runMaintenance() {
|
|
1378
1412
|
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
1413
|
+
if ((0, maintenance_policy_1.fullTableMaintenanceDisabled)()) {
|
|
1414
|
+
this.lastOptimizeDidWork = false;
|
|
1415
|
+
return this.recordSkippedCompaction((0, maintenance_policy_1.recordMaintenanceContainment)(this.lancedbDir));
|
|
1416
|
+
}
|
|
1379
1417
|
if (this.maintenanceRunning) {
|
|
1380
1418
|
(0, logger_1.debug)("vectordb", "Maintenance already running, skipping");
|
|
1381
1419
|
return (0, compaction_result_1.skippedCompaction)("maintenance already running");
|
|
@@ -1426,7 +1464,7 @@ class VectorDB {
|
|
|
1426
1464
|
// Track across both passes: `lastOptimizeDidWork` is reset per optimize()
|
|
1427
1465
|
// call, so a productive first pass followed by a barren bloat retry would
|
|
1428
1466
|
// otherwise read as unproductive and trigger a spurious backoff.
|
|
1429
|
-
|
|
1467
|
+
const didWork = this.lastOptimizeDidWork;
|
|
1430
1468
|
if (result.status === "completed" && this.lastOptimizeDidWork) {
|
|
1431
1469
|
const table = yield this.openExistingTableUnsafe();
|
|
1432
1470
|
if (table) {
|
|
@@ -1443,17 +1481,17 @@ class VectorDB {
|
|
|
1443
1481
|
const diskSize = this.getDirectorySize(this.lancedbDir);
|
|
1444
1482
|
const logicalSize = stats.totalBytes;
|
|
1445
1483
|
const bloatRatio = logicalSize > 0 ? diskSize / logicalSize : 0;
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
(0, logger_1.log)("vectordb", `Bloat detected after optimize: ${(diskSize / 1024 / 1024).toFixed(0)}MB disk vs ${(logicalSize / 1024 / 1024).toFixed(0)}MB logical (${bloatRatio.toFixed(1)}x) — retrying`);
|
|
1449
|
-
yield new Promise((r) => setTimeout(r, 2000));
|
|
1450
|
-
result = yield this.optimize(5, 0, true);
|
|
1451
|
-
didWork = didWork || this.lastOptimizeDidWork;
|
|
1484
|
+
if (bloatRatio > 2.0) {
|
|
1485
|
+
(0, logger_1.log)("vectordb", `Bloat remains after optimize: ${bloatRatio.toFixed(1)}x; additional rewrite disabled`);
|
|
1452
1486
|
}
|
|
1453
1487
|
}
|
|
1454
1488
|
}
|
|
1455
1489
|
this.noteCompaction(didWork);
|
|
1456
1490
|
}
|
|
1491
|
+
if (result && result.status !== "completed")
|
|
1492
|
+
return result;
|
|
1493
|
+
if (result === null || result === void 0 ? void 0 : result.cleanupReason)
|
|
1494
|
+
return result;
|
|
1457
1495
|
const maintainedTable = yield this.openExistingTableUnsafe();
|
|
1458
1496
|
this.maintainedTableVersion =
|
|
1459
1497
|
yield this.readTableVersion(maintainedTable);
|
|
@@ -1509,6 +1547,11 @@ class VectorDB {
|
|
|
1509
1547
|
}
|
|
1510
1548
|
compactIfNeeded() {
|
|
1511
1549
|
return __awaiter(this, arguments, void 0, function* (threshold = config_1.FRAGMENT_COMPACT_THRESHOLD) {
|
|
1550
|
+
if ((0, maintenance_policy_1.fullTableMaintenanceDisabled)()) {
|
|
1551
|
+
this.lastOptimizeDidWork = false;
|
|
1552
|
+
this.recordSkippedCompaction((0, maintenance_policy_1.recordMaintenanceContainment)(this.lancedbDir));
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1512
1555
|
if (this.maintenanceRunning)
|
|
1513
1556
|
return false;
|
|
1514
1557
|
this.maintenanceRunning = true;
|
|
@@ -35,10 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.autostartDisabledReason = autostartDisabledReason;
|
|
37
37
|
exports.isAutostartDisabled = isAutostartDisabled;
|
|
38
|
+
exports.daemonStartDeniedReason = daemonStartDeniedReason;
|
|
38
39
|
exports.autostartDisabledUndo = autostartDisabledUndo;
|
|
39
40
|
exports.autostartDisabledNotice = autostartDisabledNotice;
|
|
40
41
|
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
41
43
|
const config_1 = require("../../config");
|
|
44
|
+
const safety_latch_1 = require("./safety-latch");
|
|
42
45
|
/**
|
|
43
46
|
* Kill switch for *implicit* daemon auto-start. Create
|
|
44
47
|
* ~/.gmax/autostart-disabled (or export GMAX_NO_AUTOSTART=1) to keep session
|
|
@@ -46,8 +49,8 @@ const config_1 = require("../../config");
|
|
|
46
49
|
* reviving the daemon — needed when the host is quarantined and the daemon's
|
|
47
50
|
* write volume is the thing under investigation.
|
|
48
51
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
52
|
+
* Safety containment also gates explicit daemon launches. A launch is not
|
|
53
|
+
* authorization to remove an existing host quarantine.
|
|
51
54
|
*
|
|
52
55
|
* plugins/grepmax/hooks/watch-lease.js carries a plain-JS copy of these semantics
|
|
53
56
|
* (env var checked first, then the file) because a SessionStart hook cannot
|
|
@@ -56,16 +59,35 @@ const config_1 = require("../../config");
|
|
|
56
59
|
function autostartDisabledReason() {
|
|
57
60
|
if (process.env.GMAX_NO_AUTOSTART === "1")
|
|
58
61
|
return "env";
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if ((0, safety_latch_1.safetyStopReason)() !== null)
|
|
63
|
+
return "safety";
|
|
64
|
+
const markers = new Set([
|
|
65
|
+
config_1.PATHS.autostartDisabledFile,
|
|
66
|
+
...(config_1.PATHS.sharedRoot
|
|
67
|
+
? [path.join(config_1.PATHS.sharedRoot, "autostart-disabled")]
|
|
68
|
+
: []),
|
|
69
|
+
]);
|
|
70
|
+
for (const marker of markers) {
|
|
71
|
+
try {
|
|
72
|
+
fs.lstatSync(marker);
|
|
73
|
+
return "file";
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT")
|
|
77
|
+
return "file";
|
|
78
|
+
}
|
|
64
79
|
}
|
|
80
|
+
return null;
|
|
65
81
|
}
|
|
66
82
|
function isAutostartDisabled() {
|
|
67
83
|
return autostartDisabledReason() !== null;
|
|
68
84
|
}
|
|
85
|
+
function daemonStartDeniedReason() {
|
|
86
|
+
const safety = (0, safety_latch_1.safetyStopReason)();
|
|
87
|
+
if (safety !== null)
|
|
88
|
+
return `host safety stop: ${safety}`;
|
|
89
|
+
return isAutostartDisabled() ? "daemon startup is quarantined" : null;
|
|
90
|
+
}
|
|
69
91
|
/**
|
|
70
92
|
* One-line notice for commands that fell back to in-process work because the
|
|
71
93
|
* kill switch is on, or null when it isn't. Names the specific undo step so
|
|
@@ -76,11 +98,16 @@ function autostartDisabledUndo() {
|
|
|
76
98
|
const reason = autostartDisabledReason();
|
|
77
99
|
if (!reason)
|
|
78
100
|
return null;
|
|
101
|
+
if (reason === "safety")
|
|
102
|
+
return "review the persistent host safety stop before resuming";
|
|
79
103
|
return reason === "env"
|
|
80
104
|
? "unset GMAX_NO_AUTOSTART"
|
|
81
105
|
: `rm ${config_1.PATHS.autostartDisabledFile}`;
|
|
82
106
|
}
|
|
83
107
|
function autostartDisabledNotice() {
|
|
108
|
+
const safety = (0, safety_latch_1.safetyStopReason)();
|
|
109
|
+
if (safety !== null)
|
|
110
|
+
return `Gmax is paused by a persistent host safety stop: ${safety}. Mutations remain blocked until containment is reviewed.`;
|
|
84
111
|
// A secondary store (src/bin.ts) runs in-process by design, not because the
|
|
85
112
|
// kill switch is on, so say that instead of naming an undo step. Callers use
|
|
86
113
|
// a non-null notice as the gate, so this must stay non-null.
|
|
@@ -48,6 +48,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
48
48
|
const fs = __importStar(require("node:fs"));
|
|
49
49
|
const path = __importStar(require("node:path"));
|
|
50
50
|
const config_1 = require("../../config");
|
|
51
|
+
const autostart_1 = require("./autostart");
|
|
51
52
|
const log_rotate_1 = require("./log-rotate");
|
|
52
53
|
/**
|
|
53
54
|
* Spawn the daemon in background mode.
|
|
@@ -56,6 +57,8 @@ const log_rotate_1 = require("./log-rotate");
|
|
|
56
57
|
function spawnDaemon() {
|
|
57
58
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
59
|
var _a;
|
|
60
|
+
if ((0, autostart_1.daemonStartDeniedReason)() !== null)
|
|
61
|
+
return null;
|
|
59
62
|
let out = null;
|
|
60
63
|
try {
|
|
61
64
|
const logFile = path.join(config_1.PATHS.logsDir, "daemon.log");
|
|
@@ -90,6 +93,9 @@ function spawnDaemon() {
|
|
|
90
93
|
*/
|
|
91
94
|
function spawnDaemonProcess() {
|
|
92
95
|
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
const denied = (0, autostart_1.daemonStartDeniedReason)();
|
|
97
|
+
if (denied !== null)
|
|
98
|
+
throw new Error(`gmax: ${denied}`);
|
|
93
99
|
const logFile = path.join(config_1.PATHS.logsDir, "daemon.log");
|
|
94
100
|
const out = (0, log_rotate_1.openRotatedLog)(logFile);
|
|
95
101
|
try {
|
|
@@ -21,11 +21,15 @@ const node_child_process_1 = require("node:child_process");
|
|
|
21
21
|
* docs/2026-08-04-macos-kernel-zone-panic-incident.md.
|
|
22
22
|
*
|
|
23
23
|
* gmax cannot fix the kernel defect, but it can decline to be the thing that
|
|
24
|
-
* detonates it. Sampling the zone
|
|
25
|
-
*
|
|
24
|
+
* detonates it. Sampling the zone has taken 1.584 s on the incident host, so the
|
|
25
|
+
* probe uses a bounded five-second budget. The daemon stands down while there
|
|
26
|
+
* are still hours of headroom.
|
|
26
27
|
*/
|
|
27
28
|
/** The zone that leaks. Named explicitly so `zprint` returns a single line. */
|
|
28
29
|
const ZONE_NAME = "data.kalloc.1024";
|
|
30
|
+
// Two 500 ms probes timed out on the incident host; a subsequent probe completed
|
|
31
|
+
// in 1,584 ms. Keep the previous five-second ceiling to allow scheduling margin.
|
|
32
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
29
33
|
/**
|
|
30
34
|
* Warn threshold.
|
|
31
35
|
*
|
|
@@ -84,8 +88,8 @@ function parseZprintOutput(output, zoneName = ZONE_NAME) {
|
|
|
84
88
|
}
|
|
85
89
|
/**
|
|
86
90
|
* Sample the zone. Returns null off macOS, or whenever the sample cannot be
|
|
87
|
-
* trusted
|
|
88
|
-
*
|
|
91
|
+
* trusted. An unparseable or timed-out report reads as "unknown", never as
|
|
92
|
+
* "healthy". macOS startup and heavy-operation admission refuse unknown samples.
|
|
89
93
|
*/
|
|
90
94
|
function readKernelZoneUsage(zoneName = ZONE_NAME) {
|
|
91
95
|
if (process.platform !== "darwin")
|
|
@@ -93,7 +97,8 @@ function readKernelZoneUsage(zoneName = ZONE_NAME) {
|
|
|
93
97
|
try {
|
|
94
98
|
const output = (0, node_child_process_1.execFileSync)("zprint", [zoneName], {
|
|
95
99
|
encoding: "utf-8",
|
|
96
|
-
timeout:
|
|
100
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
101
|
+
maxBuffer: 64 * 1024,
|
|
97
102
|
stdio: ["ignore", "pipe", "ignore"],
|
|
98
103
|
});
|
|
99
104
|
const parsed = parseZprintOutput(output, zoneName);
|
|
@@ -0,0 +1,121 @@
|
|
|
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.SAFETY_LATCH_NAME = void 0;
|
|
37
|
+
exports.safetyStopReason = safetyStopReason;
|
|
38
|
+
exports.latchSafetyStop = latchSafetyStop;
|
|
39
|
+
exports.assertNoSafetyStop = assertNoSafetyStop;
|
|
40
|
+
const node_crypto_1 = require("node:crypto");
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const config_1 = require("../../config");
|
|
44
|
+
/** Machine-wide stop: changing stores or installing a version cannot clear it. */
|
|
45
|
+
exports.SAFETY_LATCH_NAME = "safety-stop.json";
|
|
46
|
+
function safetyRoot() {
|
|
47
|
+
var _a;
|
|
48
|
+
return (_a = config_1.PATHS.sharedRoot) !== null && _a !== void 0 ? _a : path.dirname(config_1.PATHS.autostartDisabledFile);
|
|
49
|
+
}
|
|
50
|
+
/** Absence is distinct from an unreadable/corrupt stop, which stays stopped. */
|
|
51
|
+
function safetyStopReason(root = safetyRoot()) {
|
|
52
|
+
const file = path.join(root, exports.SAFETY_LATCH_NAME);
|
|
53
|
+
try {
|
|
54
|
+
const stat = fs.lstatSync(file);
|
|
55
|
+
if (!stat.isFile() || stat.size > 8192)
|
|
56
|
+
return "unverified safety stop";
|
|
57
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
58
|
+
if (value.schemaVersion !== 1 ||
|
|
59
|
+
!Number.isFinite(value.at) ||
|
|
60
|
+
typeof value.reason !== "string" ||
|
|
61
|
+
!value.reason.trim())
|
|
62
|
+
return "unverified safety stop";
|
|
63
|
+
return value.reason.replace(/[\r\n\t]/g, " ").slice(0, 512);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return error.code === "ENOENT"
|
|
67
|
+
? null
|
|
68
|
+
: "unreadable safety stop";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Atomic durable latch; intentionally no automatic expiry or clear operation. */
|
|
72
|
+
function latchSafetyStop(reason, root = safetyRoot()) {
|
|
73
|
+
if (safetyStopReason(root) !== null)
|
|
74
|
+
return;
|
|
75
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
76
|
+
fs.chmodSync(root, 0o700);
|
|
77
|
+
const file = path.join(root, exports.SAFETY_LATCH_NAME);
|
|
78
|
+
const temporary = path.join(root, `.safety-stop-${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
79
|
+
let fd;
|
|
80
|
+
try {
|
|
81
|
+
fd = fs.openSync(temporary, "wx", 0o600);
|
|
82
|
+
const record = {
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
at: Date.now(),
|
|
85
|
+
reason: reason.replace(/[\r\n\t]/g, " ").slice(0, 512) || "host safety stop",
|
|
86
|
+
};
|
|
87
|
+
fs.writeFileSync(fd, `${JSON.stringify(record)}\n`);
|
|
88
|
+
fs.fsyncSync(fd);
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
fd = undefined;
|
|
91
|
+
// Publish without replacing another concurrent stop's original cause.
|
|
92
|
+
try {
|
|
93
|
+
fs.linkSync(temporary, file);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code !== "EEXIST")
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
// Persist the directory entry, not only the payload, before stopping.
|
|
100
|
+
const dir = fs.openSync(root, "r");
|
|
101
|
+
try {
|
|
102
|
+
fs.fsyncSync(dir);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
fs.closeSync(dir);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (fd !== undefined)
|
|
110
|
+
fs.closeSync(fd);
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(temporary);
|
|
113
|
+
}
|
|
114
|
+
catch (_a) { }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function assertNoSafetyStop() {
|
|
118
|
+
const reason = safetyStopReason();
|
|
119
|
+
if (reason !== null)
|
|
120
|
+
throw new Error(`gmax safety stop: ${reason}; containment must be reviewed before resuming`);
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -15,15 +15,23 @@ const SOCKET = path.join(GMAX_DIR, "daemon.sock");
|
|
|
15
15
|
// Activity refreshes this TTL; idle sessions do not watch forever.
|
|
16
16
|
const SESSION_LEASE_TTL_MS = 4 * 60 * 60 * 1000;
|
|
17
17
|
|
|
18
|
-
function
|
|
19
|
-
if (process.env.GMAX_NO_AUTOSTART === "1") return true;
|
|
18
|
+
function markerPresentOrUnknown(file) {
|
|
20
19
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
fs.lstatSync(file);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return error.code !== "ENOENT";
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isAutostartDisabled() {
|
|
28
|
+
if (process.env.GMAX_NO_AUTOSTART === "1") return true;
|
|
29
|
+
return (
|
|
30
|
+
markerPresentOrUnknown(path.join(GMAX_DIR, "autostart-disabled")) ||
|
|
31
|
+
markerPresentOrUnknown(path.join(GMAX_DIR, "safety-stop.json"))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
function readHookInput(timeoutMs = 1000) {
|
|
28
36
|
return new Promise((resolve) => {
|
|
29
37
|
let data = "";
|