grepmax 0.26.21 → 0.26.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.21",
13
+ "version": "0.26.22",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -59,6 +59,9 @@ const process_1 = require("../lib/utils/process");
59
59
  const project_registry_1 = require("../lib/utils/project-registry");
60
60
  const project_root_1 = require("../lib/utils/project-root");
61
61
  const watcher_store_1 = require("../lib/utils/watcher-store");
62
+ // Hard ceiling on signal-driven shutdown; comfortably above the daemon's own
63
+ // 30s operation-drain grace so the orderly path normally wins.
64
+ const SIGNAL_EXIT_BACKSTOP_MS = 90000;
62
65
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
63
66
  const IDLE_CHECK_INTERVAL_MS = 60 * 1000; // check every minute
64
67
  exports.watch = new commander_1.Command("watch")
@@ -157,8 +160,24 @@ exports.watch = new commander_1.Command("watch")
157
160
  installTimestampedOutput();
158
161
  const { Daemon } = yield Promise.resolve().then(() => __importStar(require("../lib/daemon/daemon")));
159
162
  const daemon = new Daemon();
160
- process.on("SIGINT", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
161
- process.on("SIGTERM", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
163
+ // A shutdown that never resolves must still end the process: both
164
+ // signals arm a hard-exit backstop, the same way the lock-compromised
165
+ // path does. Without it the daemon sat idle in uv_run for minutes after
166
+ // SIGTERM (and ignored Activity Monitor's Quit) while a remove waited on
167
+ // a store lease that could never free.
168
+ const shutdownOnSignal = (signal) => {
169
+ const backstop = setTimeout(() => {
170
+ console.error(`[daemon] shutdown did not complete within ${SIGNAL_EXIT_BACKSTOP_MS / 1000}s of ${signal} — forcing exit`);
171
+ process.exit(1);
172
+ }, SIGNAL_EXIT_BACKSTOP_MS);
173
+ backstop.unref();
174
+ daemon.shutdown().then(() => {
175
+ clearTimeout(backstop);
176
+ return (0, exit_1.gracefulExit)();
177
+ });
178
+ };
179
+ process.on("SIGINT", () => shutdownOnSignal("SIGINT"));
180
+ process.on("SIGTERM", () => shutdownOnSignal("SIGTERM"));
162
181
  process.on("uncaughtException", (err) => {
163
182
  console.error("[daemon] uncaughtException:", err);
164
183
  daemon.shutdown().then(() => (0, exit_1.gracefulExit)(1));
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
+ var _a;
47
48
  Object.defineProperty(exports, "__esModule", { value: true });
48
49
  exports.Daemon = void 0;
49
50
  const fs = __importStar(require("node:fs"));
@@ -81,6 +82,10 @@ const watcher_manager_1 = require("./watcher-manager");
81
82
  // drops, and orphan MLX cleanup. 4 hours keeps the daemon resident through a
82
83
  // normal workday while still freeing resources overnight. Override with
83
84
  // GMAX_DAEMON_IDLE_TIMEOUT_MS=<ms>; set to 0 (or negative) to disable.
85
+ // Grace period for admitted operations to settle during shutdown. See
86
+ // drainOperationsBounded. Env override exists for tests and for a user who
87
+ // would rather wait on a slow-but-progressing operation.
88
+ const SHUTDOWN_DRAIN_TIMEOUT_MS = Number((_a = process.env.GMAX_SHUTDOWN_DRAIN_TIMEOUT_MS) !== null && _a !== void 0 ? _a : 30000);
84
89
  const DEFAULT_IDLE_TIMEOUT_MS = 4 * 60 * 60 * 1000;
85
90
  const IDLE_TIMEOUT_MS = (() => {
86
91
  const raw = process.env.GMAX_DAEMON_IDLE_TIMEOUT_MS;
@@ -1755,6 +1760,40 @@ class Daemon {
1755
1760
  this.recycling = true;
1756
1761
  void this.shutdown().finally(() => process.exit(0));
1757
1762
  }
1763
+ /**
1764
+ * Wait for admitted operations to settle, but never forever. Every operation
1765
+ * has just been aborted; one that still does not return within the grace
1766
+ * period is wedged on something shutdown cannot influence, and holding the
1767
+ * daemon alive for it only leaves the user with a process that ignores
1768
+ * SIGTERM. Resources are released regardless — a wedged op loses its work
1769
+ * either way, and the next start reconciles the store.
1770
+ */
1771
+ drainOperationsBounded(...drains) {
1772
+ return __awaiter(this, void 0, void 0, function* () {
1773
+ let timer;
1774
+ const timeout = new Promise((resolve) => {
1775
+ timer = setTimeout(() => resolve("timeout"), SHUTDOWN_DRAIN_TIMEOUT_MS);
1776
+ timer.unref();
1777
+ });
1778
+ try {
1779
+ const outcome = yield Promise.race([
1780
+ Promise.all(drains).then(() => "drained"),
1781
+ timeout,
1782
+ ]);
1783
+ if (outcome === "timeout") {
1784
+ const ops = this.operations.activeOperationNames();
1785
+ const locks = this.projectMutex.pendingKeys();
1786
+ console.error(`[daemon] shutdown drain timed out after ${SHUTDOWN_DRAIN_TIMEOUT_MS / 1000}s — ` +
1787
+ `abandoning ${ops.length} operation(s) [${ops.join(", ")}] and ` +
1788
+ `${locks.length} project lock(s) [${locks.join(", ")}] to finish shutdown`);
1789
+ }
1790
+ }
1791
+ finally {
1792
+ if (timer)
1793
+ clearTimeout(timer);
1794
+ }
1795
+ });
1796
+ }
1758
1797
  shutdown(opts = {}) {
1759
1798
  if (!this.shutdownPromise) {
1760
1799
  this.shutdownPromise = this.performShutdown(opts);
@@ -1763,7 +1802,7 @@ class Daemon {
1763
1802
  }
1764
1803
  performShutdown(opts) {
1765
1804
  return __awaiter(this, void 0, void 0, function* () {
1766
- var _a, _b, _c, _d;
1805
+ var _a, _b, _c, _d, _e;
1767
1806
  this.shuttingDown = true;
1768
1807
  this.ready = false;
1769
1808
  const strictResourceShutdown = this.hasUnfinishedRebuild();
@@ -1795,18 +1834,18 @@ class Daemon {
1795
1834
  try {
1796
1835
  fs.unlinkSync(config_1.PATHS.daemonSocket);
1797
1836
  }
1798
- catch (_e) { }
1837
+ catch (_f) { }
1799
1838
  try {
1800
1839
  fs.unlinkSync(config_1.PATHS.daemonPidFile);
1801
1840
  }
1802
- catch (_f) { }
1841
+ catch (_g) { }
1803
1842
  if (this.releaseLock) {
1804
1843
  const release = this.releaseLock;
1805
1844
  this.releaseLock = null;
1806
1845
  try {
1807
1846
  yield release();
1808
1847
  }
1809
- catch (_g) { }
1848
+ catch (_h) { }
1810
1849
  }
1811
1850
  if (this.heartbeatInterval)
1812
1851
  clearInterval(this.heartbeatInterval);
@@ -1823,16 +1862,21 @@ class Daemon {
1823
1862
  for (const ac of this.shutdownAbortControllers) {
1824
1863
  ac.abort(new operation_coordinator_1.OperationClosedError());
1825
1864
  }
1865
+ // An operation blocked in StoreLease.acquireExclusive (remove, repair)
1866
+ // only listens to the VectorDB's own lease abort, which `vectorDb.close()`
1867
+ // fires — after the drain below. Fire it now, or a lease that never frees
1868
+ // (see known-limitations: recycled-PID reader marker) deadlocks shutdown.
1869
+ (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.abortLeaseWaits();
1826
1870
  const operationDrain = this.operations.close();
1827
1871
  const projectDrain = this.projectMutex.close();
1828
1872
  // Abort catchup/recovery and remove each processor before worker teardown.
1829
1873
  yield this.watcherManager.quiesceAll();
1830
- yield Promise.all([operationDrain, projectDrain]);
1874
+ yield this.drainOperationsBounded(operationDrain, projectDrain);
1831
1875
  // Stop LLM server if running
1832
1876
  try {
1833
- yield ((_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.stop());
1877
+ yield ((_b = this.llmServer) === null || _b === void 0 ? void 0 : _b.stop());
1834
1878
  }
1835
- catch (_h) { }
1879
+ catch (_j) { }
1836
1880
  // Destroy worker pool to prevent orphaned child processes
1837
1881
  if (this.workerPool) {
1838
1882
  if (strictResourceShutdown) {
@@ -1844,7 +1888,7 @@ class Daemon {
1844
1888
  try {
1845
1889
  yield this.workerPool.destroy();
1846
1890
  }
1847
- catch (_j) { }
1891
+ catch (_k) { }
1848
1892
  }
1849
1893
  this.workerPool = null;
1850
1894
  this.resources = null;
@@ -1853,7 +1897,7 @@ class Daemon {
1853
1897
  try {
1854
1898
  yield (0, pool_1.destroyWorkerPool)();
1855
1899
  }
1856
- catch (_k) { }
1900
+ catch (_l) { }
1857
1901
  }
1858
1902
  // Stop MLX embed server only after worker requests have drained.
1859
1903
  yield this.mlxServerManager.stopMlxServer();
@@ -1865,17 +1909,17 @@ class Daemon {
1865
1909
  this.processors.clear();
1866
1910
  // Close shared resources
1867
1911
  try {
1868
- yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
1912
+ yield ((_c = this.metaCache) === null || _c === void 0 ? void 0 : _c.close());
1869
1913
  }
1870
- catch (_l) { }
1914
+ catch (_m) { }
1871
1915
  if (strictResourceShutdown) {
1872
- yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close({ requireClosed: true }));
1916
+ yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close({ requireClosed: true }));
1873
1917
  }
1874
1918
  else {
1875
1919
  try {
1876
- yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
1920
+ yield ((_e = this.vectorDb) === null || _e === void 0 ? void 0 : _e.close());
1877
1921
  }
1878
- catch (_m) { }
1922
+ catch (_o) { }
1879
1923
  }
1880
1924
  yield serverClosed;
1881
1925
  clearTimeout(forceCloseConnections);
@@ -497,6 +497,17 @@ class VectorDB {
497
497
  * Run a destructive table mutation while holding the process-wide exclusive
498
498
  * store intent. New local writes stop before existing activity is drained.
499
499
  */
500
+ /**
501
+ * Cancel every in-flight wait on the store lease (exclusive-mutation
502
+ * acquisition, initial lease attach). Called by the daemon at the start of
503
+ * shutdown, *before* it drains active operations: a `remove`/`repair` that is
504
+ * polling for the exclusive lease would otherwise hold its operation slot
505
+ * until the lease deadline, and shutdown would wait on it — the lease abort
506
+ * in `close()` never fires because `close()` runs after the drain. Idempotent.
507
+ */
508
+ abortLeaseWaits() {
509
+ this.leaseAbort.abort();
510
+ }
500
511
  withExclusiveTableMutation(mutation) {
501
512
  return __awaiter(this, void 0, void 0, function* () {
502
513
  if (this.closed)
@@ -28,6 +28,12 @@ class KeyedMutex {
28
28
  }
29
29
  return count;
30
30
  }
31
+ /** Keys currently held or queued — for shutdown diagnostics. */
32
+ pendingKeys() {
33
+ return [...this.keys.entries()]
34
+ .filter(([, state]) => state.locked || state.queue.length > 0)
35
+ .map(([key]) => key);
36
+ }
31
37
  run(key, signal, fn) {
32
38
  const task = (() => __awaiter(this, void 0, void 0, function* () {
33
39
  yield this.acquire(key, signal);
@@ -39,6 +39,7 @@ class OperationCoordinator {
39
39
  this.controllers = new Set();
40
40
  this.activeTasks = new Set();
41
41
  this.sharedTasks = new Set();
42
+ this.taskNames = new Map();
42
43
  this.closePromise = null;
43
44
  }
44
45
  get status() {
@@ -47,7 +48,11 @@ class OperationCoordinator {
47
48
  get activeCount() {
48
49
  return this.activeTasks.size;
49
50
  }
50
- runShared(_name, signal, fn) {
51
+ /** Names of admitted operations that have not settled — for shutdown diagnostics. */
52
+ activeOperationNames() {
53
+ return [...this.activeTasks].map((task) => { var _a; return (_a = this.taskNames.get(task)) !== null && _a !== void 0 ? _a : "?"; });
54
+ }
55
+ runShared(name, signal, fn) {
51
56
  try {
52
57
  this.assertSharedAdmission();
53
58
  }
@@ -65,11 +70,13 @@ class OperationCoordinator {
65
70
  }))();
66
71
  this.activeTasks.add(task);
67
72
  this.sharedTasks.add(task);
73
+ this.taskNames.set(task, name);
68
74
  const cleanup = () => {
69
75
  unlink();
70
76
  this.controllers.delete(controller);
71
77
  this.activeTasks.delete(task);
72
78
  this.sharedTasks.delete(task);
79
+ this.taskNames.delete(task);
73
80
  };
74
81
  void task.then(cleanup, cleanup);
75
82
  return task;
@@ -107,7 +114,11 @@ class OperationCoordinator {
107
114
  }
108
115
  }))();
109
116
  this.activeTasks.add(task);
110
- const cleanup = () => this.activeTasks.delete(task);
117
+ this.taskNames.set(task, name);
118
+ const cleanup = () => {
119
+ this.activeTasks.delete(task);
120
+ this.taskNames.delete(task);
121
+ };
111
122
  void task.then(cleanup, cleanup);
112
123
  return task;
113
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.21",
3
+ "version": "0.26.22",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.21",
3
+ "version": "0.26.22",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",