grepmax 0.26.54 → 0.26.56
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 +201 -82
- package/dist/lib/daemon/mlx-server-manager.js +24 -8
- 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 +76 -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 +140 -21
- package/dist/lib/utils/safety-latch.js +121 -0
- package/dist/lib/workers/pool.js +97 -10
- 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.56",
|
|
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. Speculative embedding warmup and maintenance timers are disabled. On macOS, startup, heavy queries, worker forks and MLX launches require known normal OS memory pressure and a healthy kernel-zone sample; unavailable probes report bounded diagnostic details and refuse work.
|
|
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") {
|
|
@@ -57,9 +57,12 @@ const embedding_status_1 = require("../index/embedding-status");
|
|
|
57
57
|
const index_config_1 = require("../index/index-config");
|
|
58
58
|
const syncer_1 = require("../index/syncer");
|
|
59
59
|
const server_1 = require("../llm/server");
|
|
60
|
+
const compaction_result_1 = require("../store/compaction-result");
|
|
61
|
+
const maintenance_policy_1 = require("../store/maintenance-policy");
|
|
60
62
|
const meta_cache_1 = require("../store/meta-cache");
|
|
61
63
|
const store_lease_1 = require("../store/store-lease");
|
|
62
64
|
const vector_db_1 = require("../store/vector-db");
|
|
65
|
+
const autostart_1 = require("../utils/autostart");
|
|
63
66
|
const blocked_roots_1 = require("../utils/blocked-roots");
|
|
64
67
|
const daemon_client_1 = require("../utils/daemon-client");
|
|
65
68
|
const daemon_launcher_1 = require("../utils/daemon-launcher");
|
|
@@ -73,6 +76,7 @@ const process_footprint_1 = require("../utils/process-footprint");
|
|
|
73
76
|
const project_registry_1 = require("../utils/project-registry");
|
|
74
77
|
const resource_snapshot_1 = require("../utils/resource-snapshot");
|
|
75
78
|
const root_availability_1 = require("../utils/root-availability");
|
|
79
|
+
const safety_latch_1 = require("../utils/safety-latch");
|
|
76
80
|
const watcher_store_1 = require("../utils/watcher-store");
|
|
77
81
|
const pool_1 = require("../workers/pool");
|
|
78
82
|
const ipc_handler_1 = require("./ipc-handler");
|
|
@@ -148,8 +152,6 @@ class Daemon {
|
|
|
148
152
|
this.heartbeatInterval = null;
|
|
149
153
|
this.idleInterval = null;
|
|
150
154
|
this.heartbeatTick = 0;
|
|
151
|
-
/** Rate-limits the kernel-zone warn log — see checkKernelZonePressure. */
|
|
152
|
-
this.lastZoneWarnMs = 0;
|
|
153
155
|
this.shuttingDown = false;
|
|
154
156
|
this.recycling = false;
|
|
155
157
|
this.recycleDueSinceMs = null;
|
|
@@ -165,6 +167,7 @@ class Daemon {
|
|
|
165
167
|
});
|
|
166
168
|
this.mlxServerManager = new mlx_server_manager_1.MlxServerManager({
|
|
167
169
|
getShuttingDown: () => this.shuttingDown,
|
|
170
|
+
assertExpansionAdmission: () => this.assertHeavyOperationAdmission("mlx-start"),
|
|
168
171
|
});
|
|
169
172
|
this.watcherManager = new watcher_manager_1.WatcherManager({
|
|
170
173
|
processors: this.processors,
|
|
@@ -238,6 +241,16 @@ class Daemon {
|
|
|
238
241
|
start() {
|
|
239
242
|
return __awaiter(this, void 0, void 0, function* () {
|
|
240
243
|
var _a;
|
|
244
|
+
const denied = (0, autostart_1.daemonStartDeniedReason)();
|
|
245
|
+
if (denied !== null)
|
|
246
|
+
throw new Error(`gmax: ${denied}; preserving containment`);
|
|
247
|
+
// Check before stale-process cleanup, store opening, workers or model setup.
|
|
248
|
+
const pressure = this.hostPressureDeniedReason();
|
|
249
|
+
if (pressure !== null) {
|
|
250
|
+
if (pressure.measured)
|
|
251
|
+
(0, safety_latch_1.latchSafetyStop)(pressure.reason);
|
|
252
|
+
throw new Error(`gmax: ${pressure.reason}; startup safety stop`);
|
|
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
|
|
@@ -391,7 +404,7 @@ class Daemon {
|
|
|
391
404
|
const globalConfig = (_a = this.activeConfig) !== null && _a !== void 0 ? _a : (0, index_config_1.readGlobalConfig)();
|
|
392
405
|
const isAppleSilicon = process.arch === "arm64" && process.platform === "darwin";
|
|
393
406
|
if (isAppleSilicon && globalConfig.embedMode === "gpu") {
|
|
394
|
-
yield this.
|
|
407
|
+
yield this.ensureAdmittedMlxServer(globalConfig.mlxModel);
|
|
395
408
|
this.assertStartupActive();
|
|
396
409
|
}
|
|
397
410
|
this.publishResourceGeneration(globalConfig, this.activeGeneration, this.vectorDb, this.workerPool, this.mlxMode(globalConfig));
|
|
@@ -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)
|
|
@@ -486,52 +496,59 @@ class Daemon {
|
|
|
486
496
|
console.log("[daemon] Idle shutdown disabled (GMAX_DAEMON_IDLE_TIMEOUT_MS<=0)");
|
|
487
497
|
}
|
|
488
498
|
console.log(`[daemon] Started (PID: ${process.pid}, ${this.processors.size} projects)`);
|
|
489
|
-
|
|
490
|
-
// pay daemon-side cold costs:
|
|
491
|
-
// - LanceDB connection open + first openTable() (~10–15s on a 5GB
|
|
492
|
-
// index — this is the dominant cost)
|
|
493
|
-
// - FTS index "already exists" round-trip
|
|
494
|
-
// - Two parallel encodeQuery calls so the worker pool spawns + warms
|
|
495
|
-
// workers ahead of the first real search. The reaper floor is
|
|
496
|
-
// MIN_KEEP_WORKERS = 1 (pool.ts), so only one worker stays resident
|
|
497
|
-
// long-term — but the prewarm still pays the model-load cost up front.
|
|
498
|
-
// Fire-and-forget; failures are non-fatal — the next real search just
|
|
499
|
-
// pays the cost once. Delay a few seconds so we don't compete with the
|
|
500
|
-
// catchup scans dispatched on startup.
|
|
501
|
-
setTimeout(() => {
|
|
502
|
-
if (this.shuttingDown)
|
|
503
|
-
return;
|
|
504
|
-
void this.runSharedOperation("search-warmup", undefined, () => __awaiter(this, void 0, void 0, function* () {
|
|
505
|
-
const t0 = Date.now();
|
|
506
|
-
try {
|
|
507
|
-
if (this.vectorDb) {
|
|
508
|
-
yield this.vectorDb.ensureTable();
|
|
509
|
-
yield this.vectorDb.createFTSIndex();
|
|
510
|
-
yield this.vectorDb.createVectorIndex(false, 5, true);
|
|
511
|
-
}
|
|
512
|
-
const pool = this.workerPool;
|
|
513
|
-
if (!pool)
|
|
514
|
-
throw new Error("worker pool not ready");
|
|
515
|
-
// Two parallel encodes force the pool to spawn two workers and
|
|
516
|
-
// warm both (each worker loads Granite + ColBERT lazily on first
|
|
517
|
-
// encode — once warmed, subsequent encodes are ~13ms).
|
|
518
|
-
yield Promise.all([
|
|
519
|
-
pool.encodeQuery("warmup-a"),
|
|
520
|
-
pool.encodeQuery("warmup-b"),
|
|
521
|
-
]);
|
|
522
|
-
console.log(`[daemon] Search hot path pre-warmed (${Date.now() - t0}ms)`);
|
|
523
|
-
}
|
|
524
|
-
catch (err) {
|
|
525
|
-
console.log(`[daemon] Search warmup failed (non-fatal): ${err}`);
|
|
526
|
-
}
|
|
527
|
-
})).catch((err) => {
|
|
528
|
-
if (err instanceof operation_coordinator_1.OperationClosedError)
|
|
529
|
-
return;
|
|
530
|
-
console.log(`[daemon] Search warmup skipped: ${err}`);
|
|
531
|
-
});
|
|
532
|
-
}, 5000).unref();
|
|
499
|
+
this.scheduleSearchWarmup();
|
|
533
500
|
});
|
|
534
501
|
}
|
|
502
|
+
scheduleSearchWarmup() {
|
|
503
|
+
// Speculative encodes can spawn/load models even though existing FTS
|
|
504
|
+
// adoption is read-only. Containment schedules neither work nor admission.
|
|
505
|
+
if ((0, maintenance_policy_1.fullTableMaintenanceDisabled)())
|
|
506
|
+
return;
|
|
507
|
+
// Pre-warm the search hot path so the first user-facing search doesn't
|
|
508
|
+
// pay daemon-side cold costs:
|
|
509
|
+
// - LanceDB connection open + first openTable() (~10–15s on a 5GB
|
|
510
|
+
// index — this is the dominant cost)
|
|
511
|
+
// - FTS index "already exists" round-trip
|
|
512
|
+
// - Two parallel encodeQuery calls so the worker pool spawns + warms
|
|
513
|
+
// workers ahead of the first real search. The reaper floor is
|
|
514
|
+
// MIN_KEEP_WORKERS = 1 (pool.ts), so only one worker stays resident
|
|
515
|
+
// long-term — but the prewarm still pays the model-load cost up front.
|
|
516
|
+
// Fire-and-forget; failures are non-fatal — the next real search just
|
|
517
|
+
// pays the cost once. Delay a few seconds so we don't compete with the
|
|
518
|
+
// catchup scans dispatched on startup.
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
if (this.shuttingDown)
|
|
521
|
+
return;
|
|
522
|
+
void this.runSharedOperation("search-warmup", undefined, () => __awaiter(this, void 0, void 0, function* () {
|
|
523
|
+
const t0 = Date.now();
|
|
524
|
+
try {
|
|
525
|
+
if (this.vectorDb) {
|
|
526
|
+
yield this.vectorDb.ensureTable();
|
|
527
|
+
yield this.vectorDb.createFTSIndex();
|
|
528
|
+
yield this.vectorDb.createVectorIndex(false, 5, true);
|
|
529
|
+
}
|
|
530
|
+
const pool = this.workerPool;
|
|
531
|
+
if (!pool)
|
|
532
|
+
throw new Error("worker pool not ready");
|
|
533
|
+
// Two parallel encodes force the pool to spawn two workers and
|
|
534
|
+
// warm both (each worker loads Granite + ColBERT lazily on first
|
|
535
|
+
// encode — once warmed, subsequent encodes are ~13ms).
|
|
536
|
+
yield Promise.all([
|
|
537
|
+
pool.encodeQuery("warmup-a"),
|
|
538
|
+
pool.encodeQuery("warmup-b"),
|
|
539
|
+
]);
|
|
540
|
+
console.log(`[daemon] Search hot path pre-warmed (${Date.now() - t0}ms)`);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
console.log(`[daemon] Search warmup failed (non-fatal): ${err}`);
|
|
544
|
+
}
|
|
545
|
+
})).catch((err) => {
|
|
546
|
+
if (err instanceof operation_coordinator_1.OperationClosedError)
|
|
547
|
+
return;
|
|
548
|
+
console.log(`[daemon] Search warmup skipped: ${err}`);
|
|
549
|
+
});
|
|
550
|
+
}, 5000).unref();
|
|
551
|
+
}
|
|
535
552
|
watchProject(root, signal) {
|
|
536
553
|
return this.withProjectLock(root, signal, () => this.runSharedOperation("watch", signal, () => this.watchProjectWithinOperation(root)));
|
|
537
554
|
}
|
|
@@ -606,7 +623,37 @@ class Daemon {
|
|
|
606
623
|
}
|
|
607
624
|
}
|
|
608
625
|
runSharedOperation(name, signal, fn) {
|
|
609
|
-
|
|
626
|
+
const readOnly = ((0, read_verbs_1.getReadVerb)(name) !== undefined && !(0, read_verbs_1.isHeavyReadVerb)(name)) ||
|
|
627
|
+
["project-stats", "unwatch", "llm-stop"].includes(name);
|
|
628
|
+
return this.operations.runShared(name, signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
|
|
629
|
+
if (name === "store-maintenance" && (0, maintenance_policy_1.fullTableMaintenanceDisabled)()) {
|
|
630
|
+
// Retain coordinator close/exclusive checks, but never admit a disabled
|
|
631
|
+
// callback as heavy work or probe the kernel for a known no-op.
|
|
632
|
+
return (0, compaction_result_1.skippedCompaction)(maintenance_policy_1.FULL_TABLE_MAINTENANCE_DISABLED_REASON);
|
|
633
|
+
}
|
|
634
|
+
if (!readOnly)
|
|
635
|
+
this.assertHeavyOperationAdmission(name);
|
|
636
|
+
return fn(operationSignal);
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
ensureAdmittedMlxServer(model) {
|
|
640
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
641
|
+
this.assertHeavyOperationAdmission("mlx-start");
|
|
642
|
+
yield this.mlxServerManager.ensureMlxServer(model);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
assertHeavyOperationAdmission(name) {
|
|
646
|
+
const denied = (0, autostart_1.daemonStartDeniedReason)();
|
|
647
|
+
if (denied !== null)
|
|
648
|
+
throw new Error(`${name} refused: ${denied}`);
|
|
649
|
+
// Probe at operation admission, before setup/cache/worker/model side effects.
|
|
650
|
+
if (!this.checkKernelZonePressure()) {
|
|
651
|
+
throw new Error(`${name} refused: host pressure is unsafe or unknown`);
|
|
652
|
+
}
|
|
653
|
+
// Another process can quarantine the machine while the probes run.
|
|
654
|
+
const finalDenied = (0, autostart_1.daemonStartDeniedReason)();
|
|
655
|
+
if (finalDenied !== null)
|
|
656
|
+
throw new Error(`${name} refused: ${finalDenied}`);
|
|
610
657
|
}
|
|
611
658
|
/**
|
|
612
659
|
* Compact + prune on behalf of a CLI caller.
|
|
@@ -1298,6 +1345,18 @@ class Daemon {
|
|
|
1298
1345
|
(0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
|
|
1299
1346
|
return;
|
|
1300
1347
|
}
|
|
1348
|
+
try {
|
|
1349
|
+
this.assertHeavyOperationAdmission("repair");
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
(0, ipc_handler_1.writeDone)(conn, {
|
|
1353
|
+
ok: false,
|
|
1354
|
+
error: error instanceof Error
|
|
1355
|
+
? error.message
|
|
1356
|
+
: "repair safety admission failed",
|
|
1357
|
+
});
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1301
1360
|
const targetConfig = (0, index_config_1.readGlobalConfig)();
|
|
1302
1361
|
const targetGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(targetConfig);
|
|
1303
1362
|
const clientAbort = new AbortController();
|
|
@@ -1338,8 +1397,10 @@ class Daemon {
|
|
|
1338
1397
|
};
|
|
1339
1398
|
try {
|
|
1340
1399
|
const result = yield this.operations.runExclusive("repair", () => __awaiter(this, void 0, void 0, function* () {
|
|
1400
|
+
this.assertHeavyOperationAdmission("repair");
|
|
1341
1401
|
desiredWatchRoots = yield this.watcherManager.quiesceAll();
|
|
1342
1402
|
}), (operationSignal) => __awaiter(this, void 0, void 0, function* () {
|
|
1403
|
+
this.assertHeavyOperationAdmission("repair");
|
|
1343
1404
|
throwIfPreDropCancelled(operationSignal);
|
|
1344
1405
|
oldResources.vectorDb.pauseMaintenanceLoop();
|
|
1345
1406
|
this.searchers.clear();
|
|
@@ -1435,7 +1496,7 @@ class Daemon {
|
|
|
1435
1496
|
throw new Error(`rebuilt table dimension ${physicalDim !== null && physicalDim !== void 0 ? physicalDim : "missing"} does not match target ${targetGeneration.vectorDim}`);
|
|
1436
1497
|
}
|
|
1437
1498
|
if (targetConfig.embedMode === "gpu") {
|
|
1438
|
-
yield this.
|
|
1499
|
+
yield this.ensureAdmittedMlxServer(targetGeneration.mlxModel);
|
|
1439
1500
|
}
|
|
1440
1501
|
const targetMlx = this.mlxMode(targetConfig);
|
|
1441
1502
|
targetPool = this.createWorkerPool(targetGeneration, targetConfig.embedMode);
|
|
@@ -1549,7 +1610,7 @@ class Daemon {
|
|
|
1549
1610
|
: oldResources.vectorDb;
|
|
1550
1611
|
if (oldResources.config.embedMode === "gpu" &&
|
|
1551
1612
|
oldResources.mlx === "owned") {
|
|
1552
|
-
yield this.
|
|
1613
|
+
yield this.ensureAdmittedMlxServer(oldResources.embedding.mlxModel);
|
|
1553
1614
|
}
|
|
1554
1615
|
restoredPool = this.createWorkerPool(oldResources.embedding, oldResources.config.embedMode);
|
|
1555
1616
|
yield restoredDb.downgradeStoreLease();
|
|
@@ -1843,6 +1904,18 @@ class Daemon {
|
|
|
1843
1904
|
resourceSnapshot() {
|
|
1844
1905
|
return this.lastResourceSnapshot;
|
|
1845
1906
|
}
|
|
1907
|
+
runHeartbeatMaintenance(sampleFootprint) {
|
|
1908
|
+
// Both ordinary probes and deferred-recycle ticks check safety first.
|
|
1909
|
+
if (!this.checkKernelZonePressure())
|
|
1910
|
+
return;
|
|
1911
|
+
this.maybeRecycle(sampleFootprint);
|
|
1912
|
+
if (this.shuttingDown || this.recycling || !sampleFootprint)
|
|
1913
|
+
return;
|
|
1914
|
+
void this.mlxServerManager.checkMlxHealth().catch((error) => {
|
|
1915
|
+
console.error("[daemon] MLX health/restart refused:", error instanceof Error ? error.message : "unknown failure");
|
|
1916
|
+
});
|
|
1917
|
+
this.processManager.sweepOrphanWorkers();
|
|
1918
|
+
}
|
|
1846
1919
|
logResourceSnapshot(reason, footprintMb) {
|
|
1847
1920
|
var _a, _b;
|
|
1848
1921
|
var _c, _d;
|
|
@@ -1941,38 +2014,84 @@ class Daemon {
|
|
|
1941
2014
|
* This host lost three sessions that way before the compactor was identified as
|
|
1942
2015
|
* the largest write source on it.
|
|
1943
2016
|
*
|
|
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.
|
|
2017
|
+
* Warning, critical or unknown macOS readings stop mutation before another
|
|
2018
|
+
* heavy operation can begin. Persist the reason before shutdown, so hooks and
|
|
2019
|
+
* memory guards cannot revive the writer. Nothing here reclaims kernel memory.
|
|
1949
2020
|
*
|
|
1950
|
-
*
|
|
1951
|
-
*
|
|
1952
|
-
* shut the daemon down.
|
|
2021
|
+
* Non-macOS hosts have no such probe. On macOS an unavailable sample is
|
|
2022
|
+
* explicitly unknown and cannot authorize further mutation.
|
|
1953
2023
|
*/
|
|
1954
2024
|
checkKernelZonePressure() {
|
|
1955
2025
|
if (this.shuttingDown || this.recycling)
|
|
1956
|
-
return;
|
|
1957
|
-
const
|
|
1958
|
-
if (
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2026
|
+
return false;
|
|
2027
|
+
const denied = (0, autostart_1.daemonStartDeniedReason)();
|
|
2028
|
+
if (denied !== null) {
|
|
2029
|
+
// The existing quarantine is already durable; preserve its ownership and
|
|
2030
|
+
// do not replace it with a new marker that an operator must also clear.
|
|
2031
|
+
this.stopForSafety(denied, false);
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
const pressure = this.hostPressureDeniedReason();
|
|
2035
|
+
if (pressure === null)
|
|
2036
|
+
return true;
|
|
2037
|
+
this.stopForSafety(pressure.reason);
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
/** Both measurements must be known and normal before any resource expansion. */
|
|
2041
|
+
hostPressureDeniedReason() {
|
|
2042
|
+
if (process.platform !== "darwin")
|
|
2043
|
+
return null;
|
|
2044
|
+
// This inexpensive probe rejects OS warning before a worker/model starts,
|
|
2045
|
+
// or before paying the bounded but relatively slow kernel-zone probe cost.
|
|
2046
|
+
const memory = (0, kernel_zone_1.probeMemoryPressure)();
|
|
2047
|
+
if (memory.status !== "known") {
|
|
2048
|
+
return {
|
|
2049
|
+
reason: `OS memory pressure unavailable (${(0, kernel_zone_1.formatPressureProbe)(memory)})`,
|
|
2050
|
+
measured: false,
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
if (memory.pressure !== "normal") {
|
|
2054
|
+
return {
|
|
2055
|
+
reason: `${memory.pressure} OS memory pressure (${(0, kernel_zone_1.formatPressureProbe)(memory)})`,
|
|
2056
|
+
measured: true,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
const kernel = (0, kernel_zone_1.probeKernelZoneUsage)();
|
|
2060
|
+
if (kernel.status !== "known") {
|
|
2061
|
+
return {
|
|
2062
|
+
reason: `kernel pressure unavailable (${(0, kernel_zone_1.formatPressureProbe)(kernel)}); heavy work cannot be admitted`,
|
|
2063
|
+
measured: false,
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
if (kernel.usage.pressure === "ok") {
|
|
2067
|
+
// Kernel sampling can take seconds; use a new inexpensive OS observation
|
|
2068
|
+
// at the actual admission boundary, rather than trusting its first sample.
|
|
2069
|
+
const freshMemory = (0, kernel_zone_1.probeMemoryPressure)();
|
|
2070
|
+
if (freshMemory.status === "known" && freshMemory.pressure === "normal")
|
|
2071
|
+
return null;
|
|
2072
|
+
return {
|
|
2073
|
+
reason: freshMemory.status === "known"
|
|
2074
|
+
? `${freshMemory.pressure} OS memory pressure (${(0, kernel_zone_1.formatPressureProbe)(freshMemory)})`
|
|
2075
|
+
: `OS memory pressure unavailable (${(0, kernel_zone_1.formatPressureProbe)(freshMemory)})`,
|
|
2076
|
+
measured: freshMemory.status === "known",
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
reason: `${kernel.usage.pressure} kernel pressure: ${(0, kernel_zone_1.formatZoneUsage)(kernel.usage)} (${(0, kernel_zone_1.formatPressureProbe)(kernel)})`,
|
|
2081
|
+
measured: true,
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
stopForSafety(reason, persist = true) {
|
|
2085
|
+
// Warning already pauses mutation: a stale index costs less than another
|
|
2086
|
+
// host crash. Unknown readings are visible stops, never healthy samples.
|
|
2087
|
+
try {
|
|
2088
|
+
if (persist)
|
|
2089
|
+
(0, safety_latch_1.latchSafetyStop)(reason);
|
|
2090
|
+
}
|
|
2091
|
+
catch (error) {
|
|
2092
|
+
console.error("[daemon] Failed to persist safety stop; stopping anyway:", error);
|
|
1973
2093
|
}
|
|
1974
|
-
console.log(`[daemon]
|
|
1975
|
-
`Reboot to reclaim the zone; the index will catch up on next start.`);
|
|
2094
|
+
console.log(`[daemon] SAFETY STOP: ${reason}. Containment stays in place across restart; review host health before resuming.`);
|
|
1976
2095
|
this.recycling = true;
|
|
1977
2096
|
void this.shutdown().finally(() => process.exit(0));
|
|
1978
2097
|
}
|