grepmax 0.26.41 → 0.26.43

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.41",
13
+ "version": "0.26.43",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -450,6 +450,12 @@ gmax watch restart # Restart daemon
450
450
 
451
451
  `gmax doctor` reports ANN index state. `ANN: vector index not built` is normal with the default exact-search configuration.
452
452
 
453
+ Compaction pauses index writes, waits for pending writes to commit, and opens a fresh table snapshot for each attempt. Each optimize call makes at most two attempts. Before each attempt it checks current free space against twice the table's logical size plus the critical-space reserve (5 GB by default). If there is insufficient headroom, it skips the rewrite and logs the required space; search remains available. A successful prune can reclaim fragment copies left by failed attempts.
454
+
455
+ `~/.gmax/logs/daemon.log` records compaction attempts and outcomes, including duration, logical size, disk size before/after, free space, and bytes reclaimed. `gmax status --json` exposes the last outcome under `daemon.compaction`; its `at` timestamp identifies when it was recorded. Five-minute resource snapshots track footprint, RSS, heap, external/ArrayBuffer memory, Lance cache, workers and pending work for memory investigations. Reading status uses retained snapshots and does not scan the index directory. Logs rotate to `daemon.log.prev`.
456
+
457
+ `gmax doctor --fix` reports completed, skipped, failed, or unverified optimization. It exits nonzero when a requested optimization did not complete or an older daemon could not verify its outcome. A busy or failed daemon is never retried as a second writer in the CLI process.
458
+
453
459
  ### Known issues
454
460
 
455
461
  Some log output looks alarming but is expected and self-healing. Before filing a bug, check
@@ -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 doctor_optimize_1 = require("../lib/utils/doctor-optimize");
54
55
  const doctor_status_1 = require("../lib/utils/doctor-status");
55
56
  const exit_1 = require("../lib/utils/exit");
56
57
  const lock_1 = require("../lib/utils/lock");
@@ -343,7 +344,7 @@ exports.doctor = new commander_1.Command("doctor")
343
344
  .option("--agent", "Compact output for AI agents", false)
344
345
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
345
346
  var _a, _b;
346
- var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
347
+ var _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
347
348
  if (!opts.agent)
348
349
  console.log("gmax Doctor\n");
349
350
  const root = config_1.PATHS.globalRoot;
@@ -572,7 +573,7 @@ exports.doctor = new commander_1.Command("doctor")
572
573
  ? "LOW"
573
574
  : "ok";
574
575
  }
575
- catch (_q) { }
576
+ catch (_p) { }
576
577
  const staleChunkerProjects = projects.filter((p) => { var _a; return p.status === "indexed" &&
577
578
  ((_a = p.chunkerVersion) !== null && _a !== void 0 ? _a : 1) < config_1.CONFIG.CHUNKER_VERSION; });
578
579
  const staleEmbeddingProjects = projects.filter((p) => p.status === "indexed" &&
@@ -787,7 +788,7 @@ exports.doctor = new commander_1.Command("doctor")
787
788
  yield mc.close();
788
789
  }
789
790
  }
790
- catch (_r) { }
791
+ catch (_q) { }
791
792
  }
792
793
  }
793
794
  // --fix auto-remediation
@@ -820,34 +821,22 @@ exports.doctor = new commander_1.Command("doctor")
820
821
  // daemon up it owns compaction; without one we are the sole process
821
822
  // and can do it in-process safely.
822
823
  const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
823
- const resp = yield sendDaemonCommand({ cmd: "optimize" }, { timeoutMs: 10 * 60 * 1000 });
824
- let via = "daemon";
825
- if (!resp.ok) {
826
- // Only fall back when the daemon could not take it. A daemon that
827
- // answered with a real failure should surface, not be retried here
828
- // as a competing writer.
829
- const reason = String((_l = resp.error) !== null && _l !== void 0 ? _l : "");
830
- // Fall back ONLY when nothing is listening. A daemon that is merely
831
- // too old ("unknown command") or busy ("timeout") is still a live
832
- // writer on the store, so optimizing here would recreate the
833
- // two-writer case this routing exists to prevent.
834
- const daemonAbsent = reason.includes("ECONNREFUSED") || reason.includes("ENOENT");
835
- if (daemonAbsent) {
836
- yield db.optimize(3, 0);
837
- via = "in-process (no daemon)";
838
- }
839
- else if (reason.includes("unknown command")) {
840
- via = "";
841
- console.log("WARN Optimize skipped: the running daemon predates the optimize command. Restart it (gmax watch --daemon -b) and re-run.");
824
+ // Release the diagnostic reader before the daemon prunes old files.
825
+ yield db.close();
826
+ const report = yield (0, doctor_optimize_1.runDoctorOptimize)(() => sendDaemonCommand({ cmd: "optimize" }, { timeoutMs: 10 * 60 * 1000 }), () => __awaiter(void 0, void 0, void 0, function* () {
827
+ const repairDb = new VectorDB(config_1.PATHS.lancedbDir);
828
+ try {
829
+ return yield repairDb.optimize();
842
830
  }
843
- else {
844
- via = "";
845
- console.log(`WARN Optimize failed via daemon: ${reason}`);
831
+ finally {
832
+ yield repairDb.close();
846
833
  }
847
- }
848
- if (via && !opts.agent)
849
- console.log(`ok Optimize complete (${via})`);
850
- fixed++;
834
+ }));
835
+ console.log((0, doctor_optimize_1.formatDoctorOptimize)(report, opts.agent));
836
+ if (report.status === "completed")
837
+ fixed++;
838
+ else
839
+ process.exitCode = 1;
851
840
  }
852
841
  // Only roots whose volume is mounted and whose directory is still gone
853
842
  // reach this list — projects on an unplugged drive are classified
@@ -877,7 +866,7 @@ exports.doctor = new commander_1.Command("doctor")
877
866
  const done = yield sendStreamingCommand({ cmd: "index", root: p.root, reset: true }, () => { }).catch((e) => ({ ok: false, error: String(e) }));
878
867
  if (done.ok &&
879
868
  (!("degraded" in done) || done.degraded !== true)) {
880
- const chunks = (_m = done.indexed) !== null && _m !== void 0 ? _m : 0;
869
+ const chunks = (_l = done.indexed) !== null && _l !== void 0 ? _l : 0;
881
870
  if (opts.agent) {
882
871
  console.log(`stale_chunker_reindexed\tname=${name}\tchunks=${chunks}`);
883
872
  }
@@ -889,14 +878,14 @@ exports.doctor = new commander_1.Command("doctor")
889
878
  else if (!opts.agent) {
890
879
  const degraded = "degraded" in done && done.degraded === true;
891
880
  const reason = degraded
892
- ? `${(_o = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _o !== void 0 ? _o : 0} scan errors, ${(_p = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _p !== void 0 ? _p : 0} file failures`
881
+ ? `${(_m = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _m !== void 0 ? _m : 0} scan errors, ${(_o = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _o !== void 0 ? _o : 0} file failures`
893
882
  : done.error;
894
883
  console.log(`FAIL ${name}: reindex failed (${reason})`);
895
884
  }
896
885
  }
897
886
  }
898
887
  }
899
- if (fixed === 0) {
888
+ if (fixed === 0 && !needsOptimize) {
900
889
  if (!opts.agent)
901
890
  console.log("ok Nothing to fix");
902
891
  }
@@ -88,6 +88,7 @@ const searcher_1 = require("../lib/search/searcher");
88
88
  const annotator_1 = require("../lib/skeleton/annotator");
89
89
  const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
90
90
  const symbol_extractor_1 = require("../lib/skeleton/symbol-extractor");
91
+ const compaction_result_1 = require("../lib/store/compaction-result");
91
92
  const meta_cache_1 = require("../lib/store/meta-cache");
92
93
  const cross_project_1 = require("../lib/utils/cross-project");
93
94
  const daemon_client_1 = require("../lib/utils/daemon-client");
@@ -1786,6 +1787,7 @@ exports.mcp = new commander_1.Command("mcp")
1786
1787
  ? `Last indexed: ${currentProject.lastIndexed}`
1787
1788
  : "",
1788
1789
  watcherLine,
1790
+ (0, compaction_result_1.formatCompactionStatus)(daemonStatus.compaction),
1789
1791
  `Projects: ${projects.length} indexed (call list_projects for names + per-project chunk counts)`,
1790
1792
  ].filter(Boolean);
1791
1793
  return ok(lines.join("\n"));
@@ -96,7 +96,9 @@ function loadStatusView(projects) {
96
96
  chunkCounts.set(project.root, stats.chunks);
97
97
  }
98
98
  }
99
- const daemon = Object.assign(Object.assign({}, (resp.resources !== undefined
99
+ const daemon = Object.assign(Object.assign(Object.assign({}, (resp.compaction !== undefined
100
+ ? { compaction: resp.compaction }
101
+ : {})), (resp.resources !== undefined
100
102
  ? { resources: resp.resources }
101
103
  : {})), { pid: typeof resp.pid === "number" ? resp.pid : null, uptimeSec: typeof resp.uptime === "number" ? resp.uptime : null,
102
104
  // Older daemons do not report these.
@@ -200,7 +202,7 @@ function buildStatusJson(input) {
200
202
  : null);
201
203
  }
202
204
  return {
203
- daemon: Object.assign({ running: d !== undefined, pid: (_d = d === null || d === void 0 ? void 0 : d.pid) !== null && _d !== void 0 ? _d : null, since: (d === null || d === void 0 ? void 0 : d.uptimeSec) != null ? now - d.uptimeSec * 1000 : null, workerThreads: (_e = d === null || d === void 0 ? void 0 : d.workerThreads) !== null && _e !== void 0 ? _e : null }, ((d === null || d === void 0 ? void 0 : d.resources) !== undefined ? { resources: d.resources } : {})),
205
+ daemon: Object.assign(Object.assign({ running: d !== undefined, pid: (_d = d === null || d === void 0 ? void 0 : d.pid) !== null && _d !== void 0 ? _d : null, since: (d === null || d === void 0 ? void 0 : d.uptimeSec) != null ? now - d.uptimeSec * 1000 : null, workerThreads: (_e = d === null || d === void 0 ? void 0 : d.workerThreads) !== null && _e !== void 0 ? _e : null }, ((d === null || d === void 0 ? void 0 : d.resources) !== undefined ? { resources: d.resources } : {})), ((d === null || d === void 0 ? void 0 : d.compaction) !== undefined ? { compaction: d.compaction } : {})),
204
206
  settings: {
205
207
  embedMode: globalConfig.embedMode,
206
208
  modelTier: globalConfig.modelTier,
@@ -609,13 +609,23 @@ class Daemon {
609
609
  * fall back to in-process only when no daemon answers, and are then the sole
610
610
  * writer themselves.
611
611
  */
612
+ compactionStatus() {
613
+ var _a;
614
+ var _b;
615
+ return (_b = (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.compactionStatus()) !== null && _b !== void 0 ? _b : null;
616
+ }
612
617
  runOptimize() {
613
618
  return __awaiter(this, void 0, void 0, function* () {
619
+ var _a;
614
620
  const db = this.vectorDb;
615
621
  if (!db)
616
622
  throw new Error("daemon resources not ready");
617
- yield this.runSharedOperation("store-maintenance", undefined, () => db.runMaintenance({ force: true }));
618
- return { ok: true };
623
+ const compaction = yield this.runSharedOperation("store-maintenance", undefined, () => db.runMaintenance({ force: true }));
624
+ if (!compaction)
625
+ return { ok: false, error: "maintenance produced no compaction outcome" };
626
+ return Object.assign({ ok: compaction.status === "completed", compaction }, (compaction.status === "completed"
627
+ ? {}
628
+ : { error: (_a = compaction.reason) !== null && _a !== void 0 ? _a : compaction.status }));
619
629
  });
620
630
  }
621
631
  operationStatus() {
@@ -1867,13 +1877,18 @@ class Daemon {
1867
1877
  if (this.recycleDueSinceMs === null &&
1868
1878
  !ageExceeded &&
1869
1879
  sampleFootprint &&
1870
- RSS_WATERMARK_MB > 0 &&
1871
- // A fresh daemon above the watermark would recycle in a loop; give the
1872
- // caches a chance to settle before judging the footprint.
1873
- ageMs > FOOTPRINT_RECYCLE_MIN_AGE_MS) {
1880
+ RSS_WATERMARK_MB > 0) {
1874
1881
  footprintMb = (0, process_footprint_1.readFootprintMb)();
1875
1882
  }
1876
- const footprintExceeded = footprintMb !== null && footprintMb > RSS_WATERMARK_MB;
1883
+ // The existing five-minute probe also records a baseline for memory soaks;
1884
+ // the intervening heartbeat ticks add no footprint probe or snapshot.
1885
+ if (sampleFootprint)
1886
+ this.logResourceSnapshot("periodic", footprintMb);
1887
+ const footprintExceeded = footprintMb !== null &&
1888
+ footprintMb > RSS_WATERMARK_MB &&
1889
+ // Sampling young daemons is useful; recycling them would loop before
1890
+ // their caches have had a chance to settle.
1891
+ ageMs > FOOTPRINT_RECYCLE_MIN_AGE_MS;
1877
1892
  if (this.recycleDueSinceMs === null) {
1878
1893
  if (!ageExceeded && !footprintExceeded)
1879
1894
  return;
@@ -122,8 +122,8 @@ function startHeartbeat(conn, intervalMs = HEARTBEAT_INTERVAL_MS) {
122
122
  */
123
123
  function handleCommand(daemon, cmd, conn) {
124
124
  return __awaiter(this, void 0, void 0, function* () {
125
- var _a;
126
- var _b, _c, _d, _e, _f, _g, _h, _j;
125
+ var _a, _b;
126
+ var _c, _d, _e, _f, _g, _h, _j, _k, _l;
127
127
  try {
128
128
  (0, logger_1.debug)("daemon", `ipc cmd=${cmd.cmd}${cmd.root ? ` root=${cmd.root}` : ""}`);
129
129
  if (cmd.cmd !== "ping" &&
@@ -186,7 +186,8 @@ function handleCommand(daemon, cmd, conn) {
186
186
  diskPressure: daemon.getDiskPressure(),
187
187
  mlx: daemon.getMlxStatus(),
188
188
  workers: daemon.workerCount(),
189
- resources: (_b = (_a = daemon.resourceSnapshot) === null || _a === void 0 ? void 0 : _a.call(daemon)) !== null && _b !== void 0 ? _b : null,
189
+ resources: (_c = (_a = daemon.resourceSnapshot) === null || _a === void 0 ? void 0 : _a.call(daemon)) !== null && _c !== void 0 ? _c : null,
190
+ compaction: (_d = (_b = daemon.compactionStatus) === null || _b === void 0 ? void 0 : _b.call(daemon)) !== null && _d !== void 0 ? _d : null,
190
191
  workerThreads: config_1.WORKER_THREADS_SETTING,
191
192
  };
192
193
  case "optimize":
@@ -202,14 +203,14 @@ function handleCommand(daemon, cmd, conn) {
202
203
  return Object.assign({ ok: true }, (yield daemon.projectStats(root)));
203
204
  }
204
205
  case "shutdown": {
205
- const reason = String((_c = cmd.reason) !== null && _c !== void 0 ? _c : "unknown");
206
- const fromPid = (_d = cmd.from_pid) !== null && _d !== void 0 ? _d : "?";
207
- const fromPpid = (_e = cmd.from_ppid) !== null && _e !== void 0 ? _e : "?";
208
- const fromVer = (_f = cmd.from_version) !== null && _f !== void 0 ? _f : "?";
206
+ const reason = String((_e = cmd.reason) !== null && _e !== void 0 ? _e : "unknown");
207
+ const fromPid = (_f = cmd.from_pid) !== null && _f !== void 0 ? _f : "?";
208
+ const fromPpid = (_g = cmd.from_ppid) !== null && _g !== void 0 ? _g : "?";
209
+ const fromVer = (_h = cmd.from_version) !== null && _h !== void 0 ? _h : "?";
209
210
  const fromArgv = Array.isArray(cmd.from_argv)
210
211
  ? cmd.from_argv.join(" ")
211
212
  : "?";
212
- const fromParentCmd = (_g = cmd.from_parent_cmd) !== null && _g !== void 0 ? _g : "?";
213
+ const fromParentCmd = (_j = cmd.from_parent_cmd) !== null && _j !== void 0 ? _j : "?";
213
214
  console.log(`[daemon] shutdown command received via IPC: reason=${reason} from_pid=${fromPid} from_ppid=${fromPpid} from_version=${fromVer} from_argv=[${fromArgv}] from_parent_cmd=[${fromParentCmd}]`);
214
215
  setImmediate(() => daemon.shutdown());
215
216
  return { ok: true };
@@ -274,8 +275,8 @@ function handleCommand(daemon, cmd, conn) {
274
275
  return { ok: false, error: "invalid project_roots" };
275
276
  }
276
277
  for (const prefix of [
277
- ...((_h = filters === null || filters === void 0 ? void 0 : filters.inPrefixes) !== null && _h !== void 0 ? _h : []),
278
- ...((_j = filters === null || filters === void 0 ? void 0 : filters.excludePrefixes) !== null && _j !== void 0 ? _j : []),
278
+ ...((_k = filters === null || filters === void 0 ? void 0 : filters.inPrefixes) !== null && _k !== void 0 ? _k : []),
279
+ ...((_l = filters === null || filters === void 0 ? void 0 : filters.excludePrefixes) !== null && _l !== void 0 ? _l : []),
279
280
  ]) {
280
281
  (0, path_containment_1.resolveContainedPath)(projectRoot, prefix);
281
282
  }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.skippedCompaction = skippedCompaction;
4
+ exports.parseCompactionResult = parseCompactionResult;
5
+ exports.formatCompactionStatus = formatCompactionStatus;
6
+ function skippedCompaction(reason) {
7
+ return {
8
+ status: "skipped",
9
+ reason,
10
+ at: Date.now(),
11
+ attempts: 0,
12
+ elapsedMs: 0,
13
+ };
14
+ }
15
+ function parseCompactionResult(value) {
16
+ if (!value || typeof value !== "object")
17
+ return undefined;
18
+ const r = value;
19
+ if (!["completed", "skipped", "failed"].includes(r.status) ||
20
+ !Number.isFinite(r.at) ||
21
+ Number.isNaN(new Date(r.at).getTime()) ||
22
+ !Number.isFinite(r.attempts) ||
23
+ !Number.isFinite(r.elapsedMs) ||
24
+ (r.reason !== undefined && typeof r.reason !== "string"))
25
+ return undefined;
26
+ return r;
27
+ }
28
+ function formatCompactionStatus(value) {
29
+ var _a;
30
+ const result = parseCompactionResult(value);
31
+ if (!result)
32
+ return undefined;
33
+ const reason = (_a = result.reason) === null || _a === void 0 ? void 0 : _a.replace(/[\r\n\t]/g, " ").slice(0, 512);
34
+ return `Last compaction: ${result.status} at ${new Date(result.at).toISOString()} (${result.attempts} attempts, ${result.elapsedMs}ms)${reason ? ` — ${reason}` : ""}`;
35
+ }
@@ -57,6 +57,7 @@ const cleanup_1 = require("../utils/cleanup");
57
57
  const filter_builder_1 = require("../utils/filter-builder");
58
58
  const logger_1 = require("../utils/logger");
59
59
  const ann_config_1 = require("./ann-config");
60
+ const compaction_result_1 = require("./compaction-result");
60
61
  const store_lease_1 = require("./store-lease");
61
62
  /**
62
63
  * Find abandoned Lance scratch files under `<lancedbDir>/ *.lance/data`.
@@ -188,6 +189,9 @@ const COMPACTION_MIN_INTERVAL_MS = 30 * 60 * 1000;
188
189
  * so a store that cannot be improved stops paying for the attempt.
189
190
  */
190
191
  const COMPACTION_MAX_INTERVAL_MS = 6 * 60 * 60 * 1000;
192
+ // Failed rewrites leave complete fragment copies until a later successful prune.
193
+ // One fresh-snapshot retry is enough; five attempts stranded ~70 GB in Oct 2026.
194
+ const COMPACTION_MAX_ATTEMPTS = 2;
191
195
  /**
192
196
  * Bounds for LanceDB's per-connection caches.
193
197
  *
@@ -227,6 +231,7 @@ class VectorDB {
227
231
  /** Only an index owner may create/rebuild shared indexes — see markIndexOwner. */
228
232
  this.indexOwner = false;
229
233
  this.lastOptimizeDidWork = false;
234
+ this.lastCompactionResult = null;
230
235
  /** Compaction rate limiter — see COMPACTION_MIN_INTERVAL_MS. */
231
236
  this.lastCompactionMs = 0;
232
237
  this.compactionIntervalMs = COMPACTION_MIN_INTERVAL_MS;
@@ -1082,8 +1087,18 @@ class VectorDB {
1082
1087
  }
1083
1088
  });
1084
1089
  }
1090
+ compactionStatus() {
1091
+ return this.lastCompactionResult ? Object.assign({}, this.lastCompactionResult) : null;
1092
+ }
1093
+ recordSkippedCompaction(reason) {
1094
+ const result = (0, compaction_result_1.skippedCompaction)(reason);
1095
+ this.lastCompactionResult = result;
1096
+ (0, logger_1.log)("vectordb", `Compaction result: ${JSON.stringify(result)}`);
1097
+ return Object.assign({}, result);
1098
+ }
1085
1099
  optimize() {
1086
- return __awaiter(this, arguments, void 0, function* (retries = 5, retentionMs = 0, bypassExclusiveMutation = false) {
1100
+ return __awaiter(this, arguments, void 0, function* (retries = COMPACTION_MAX_ATTEMPTS, retentionMs = 0, bypassExclusiveMutation = false) {
1101
+ var _a;
1087
1102
  if (!bypassExclusiveMutation) {
1088
1103
  while (this.exclusiveMutationPromise)
1089
1104
  yield this.exclusiveMutationPromise;
@@ -1093,7 +1108,7 @@ class VectorDB {
1093
1108
  if (this.compactingPromise) {
1094
1109
  (0, logger_1.debug)("vectordb", "Optimize already in progress, skipping");
1095
1110
  yield this.compactingPromise;
1096
- return;
1111
+ return ((_a = this.compactionStatus()) !== null && _a !== void 0 ? _a : (0, compaction_result_1.skippedCompaction)("compaction unavailable"));
1097
1112
  }
1098
1113
  this.lastOptimizeDidWork = false;
1099
1114
  let resolveCompacting;
@@ -1101,13 +1116,59 @@ class VectorDB {
1101
1116
  resolveCompacting = resolve;
1102
1117
  });
1103
1118
  this.activeCompactions++;
1119
+ const startedAt = Date.now();
1120
+ let attempts = 0;
1121
+ let logicalBytes;
1122
+ let diskBytesBefore;
1123
+ let freeBytesBefore;
1124
+ const finish = (status, reason, bytesReclaimed) => {
1125
+ const result = {
1126
+ status,
1127
+ at: Date.now(),
1128
+ attempts,
1129
+ elapsedMs: Date.now() - startedAt,
1130
+ reason: reason === null || reason === void 0 ? void 0 : reason.replace(/[\r\n\t]/g, " ").slice(0, 512),
1131
+ logicalBytes,
1132
+ diskBytesBefore,
1133
+ freeBytesBefore,
1134
+ diskBytesAfter: this.getDirectorySize(this.lancedbDir),
1135
+ freeBytesAfter: this.getAvailableBytes(),
1136
+ bytesReclaimed,
1137
+ };
1138
+ this.lastCompactionResult = result;
1139
+ (0, logger_1.log)("vectordb", `Compaction result: ${JSON.stringify(result)}`);
1140
+ return Object.assign({}, result);
1141
+ };
1104
1142
  try {
1105
- const table = yield this.ensureTableUnsafe();
1106
- const cutoff = new Date(Date.now() - retentionMs);
1143
+ const maxAttempts = Math.min(retries, COMPACTION_MAX_ATTEMPTS);
1107
1144
  let rebuiltFts = false;
1108
- for (let attempt = 1; attempt <= retries; attempt++) {
1145
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1109
1146
  yield this.drainWrites();
1110
1147
  try {
1148
+ // Open AFTER outstanding writes commit, and reopen on every retry.
1149
+ // A handle opened before drainWrites carries the old snapshot even
1150
+ // though new writes are gated. Reusing it repeats the same conflict.
1151
+ const table = yield this.ensureTableUnsafe();
1152
+ const { totalBytes } = yield table.stats();
1153
+ if (!Number.isFinite(totalBytes) || totalBytes < 0) {
1154
+ throw new Error("Cannot estimate compaction size from table stats");
1155
+ }
1156
+ // Bypass the cached pressure check: a failed attempt can consume a
1157
+ // whole table within its 30s cache window. Budget two logical copies
1158
+ // for rewrite/index overhead, leaving the critical reserve untouched.
1159
+ const availableBytes = this.getAvailableBytes();
1160
+ const requiredBytes = totalBytes * 2 + config_1.DISK_CRITICAL_BYTES;
1161
+ logicalBytes = totalBytes;
1162
+ const diskBytes = this.getDirectorySize(this.lancedbDir);
1163
+ diskBytesBefore !== null && diskBytesBefore !== void 0 ? diskBytesBefore : (diskBytesBefore = diskBytes);
1164
+ freeBytesBefore !== null && freeBytesBefore !== void 0 ? freeBytesBefore : (freeBytesBefore = availableBytes);
1165
+ if (availableBytes < requiredBytes) {
1166
+ const reason = `insufficient rewrite headroom (${(availableBytes / Math.pow(1024, 3)).toFixed(1)}GB free, ${(requiredBytes / Math.pow(1024, 3)).toFixed(1)}GB required)`;
1167
+ (0, logger_1.log)("vectordb", `Optimize skipped: ${reason}`);
1168
+ return finish("skipped", reason);
1169
+ }
1170
+ attempts++;
1171
+ (0, logger_1.log)("vectordb", `Compaction attempt: ${JSON.stringify({ attempt, maxAttempts, logicalBytes, diskBytes, freeBytes: availableBytes, requiredBytes })}`);
1111
1172
  const done = (0, logger_1.timer)("vectordb", "optimize");
1112
1173
  // deleteUnverified deletes files Lance cannot prove are unreferenced.
1113
1174
  // LanceDB's own docs: only safe "if you can guarantee that no other
@@ -1119,7 +1180,8 @@ class VectorDB {
1119
1180
  // Anything new that calls optimize() must preserve that: either be the
1120
1181
  // daemon, or be the only process on the store.
1121
1182
  const stats = yield table.optimize({
1122
- cleanupOlderThan: cutoff,
1183
+ // Include fragment copies left by earlier failed attempts.
1184
+ cleanupOlderThan: new Date(Date.now() - retentionMs),
1123
1185
  deleteUnverified: true,
1124
1186
  });
1125
1187
  done();
@@ -1136,24 +1198,25 @@ class VectorDB {
1136
1198
  else {
1137
1199
  (0, logger_1.debug)("vectordb", "Optimize: nothing to compact or prune");
1138
1200
  }
1139
- return;
1201
+ return finish("completed", undefined, prune.bytesRemoved);
1140
1202
  }
1141
1203
  catch (e) {
1142
1204
  const msg = e instanceof Error ? e.message : String(e);
1143
1205
  if (msg.includes("Nothing to do")) {
1144
1206
  (0, logger_1.debug)("vectordb", "Optimize: nothing to do");
1145
- return;
1207
+ return finish("completed", "nothing to compact or prune", 0);
1146
1208
  }
1209
+ (0, logger_1.log)("vectordb", `Compaction attempt failed: ${JSON.stringify({ attempt, elapsedMs: Date.now() - startedAt, diskBytes: this.getDirectorySize(this.lancedbDir), freeBytes: this.getAvailableBytes(), reason: msg.slice(0, 512) })}`);
1147
1210
  // ENOSPC: return immediately — retrying will only make things worse
1148
1211
  if (msg.includes("No space left on device") ||
1149
1212
  msg.includes("os error 28")) {
1150
1213
  (0, logger_1.log)("vectordb", `Optimize failed (ENOSPC): disk full — skipping retries`);
1151
- return;
1214
+ return finish("failed", "disk full — skipping retries");
1152
1215
  }
1153
- if (attempt < retries &&
1216
+ if (attempt < maxAttempts &&
1154
1217
  (msg.includes("conflict") || msg.includes("Retryable"))) {
1155
1218
  const delay = 1000 * Math.pow(2, (attempt - 1));
1156
- (0, logger_1.log)("vectordb", `Optimize conflict (attempt ${attempt}/${retries}), retrying in ${delay}ms`);
1219
+ (0, logger_1.log)("vectordb", `Optimize conflict (attempt ${attempt}/${maxAttempts}), retrying with a fresh snapshot in ${delay}ms`);
1157
1220
  yield new Promise((r) => setTimeout(r, delay));
1158
1221
  continue;
1159
1222
  }
@@ -1165,7 +1228,7 @@ class VectorDB {
1165
1228
  // Unsafe variant: the public createFTSIndex waits on
1166
1229
  // compactingPromise, which we hold.
1167
1230
  if (msg.includes("Panic")) {
1168
- if (attempt < retries &&
1231
+ if (attempt < maxAttempts &&
1169
1232
  !rebuiltFts &&
1170
1233
  !this.ftsPanicRecoveryExhausted) {
1171
1234
  rebuiltFts = true;
@@ -1178,7 +1241,7 @@ class VectorDB {
1178
1241
  catch (rebuildErr) {
1179
1242
  this.ftsPanicRecoveryExhausted = true;
1180
1243
  (0, logger_1.log)("vectordb", `FTS rebuild failed: ${rebuildErr}`);
1181
- return;
1244
+ return finish("failed", `FTS rebuild failed: ${rebuildErr}`);
1182
1245
  }
1183
1246
  }
1184
1247
  if (rebuiltFts) {
@@ -1187,9 +1250,10 @@ class VectorDB {
1187
1250
  }
1188
1251
  }
1189
1252
  (0, logger_1.log)("vectordb", `Optimize failed: ${msg}`);
1190
- return;
1253
+ return finish("failed", msg);
1191
1254
  }
1192
1255
  }
1256
+ return finish("skipped", "no compaction attempts requested");
1193
1257
  }
1194
1258
  finally {
1195
1259
  this.compactingPromise = null;
@@ -1212,16 +1276,17 @@ class VectorDB {
1212
1276
  return __awaiter(this, arguments, void 0, function* (options = {}) {
1213
1277
  if (this.maintenanceRunning) {
1214
1278
  (0, logger_1.debug)("vectordb", "Maintenance already running, skipping");
1215
- return;
1279
+ return (0, compaction_result_1.skippedCompaction)("maintenance already running");
1216
1280
  }
1217
1281
  this.maintenanceRunning = true;
1218
1282
  const epochSnapshot = this.writeEpoch;
1283
+ let result;
1219
1284
  try {
1220
1285
  const pressure = this.checkDiskPressure();
1221
1286
  if (pressure === "critical") {
1222
1287
  const freeGb = (this.getAvailableBytes() / 1024 / 1024 / 1024).toFixed(1);
1223
1288
  (0, logger_1.log)("vectordb", `Maintenance skipped: disk critically low (${freeGb}GB free)`);
1224
- return;
1289
+ return this.recordSkippedCompaction(`disk critically low (${freeGb}GB free)`);
1225
1290
  }
1226
1291
  if (!options.force &&
1227
1292
  epochSnapshot === this.maintainedEpoch &&
@@ -1250,12 +1315,12 @@ class VectorDB {
1250
1315
  }
1251
1316
  else if (pressure === "low") {
1252
1317
  (0, logger_1.log)("vectordb", `Low disk — single-pass optimize (no bloat retry)`);
1253
- yield this.optimize(1, 0, true);
1318
+ result = yield this.optimize(1, 0, true);
1254
1319
  this.noteCompaction(this.lastOptimizeDidWork);
1255
1320
  }
1256
1321
  else {
1257
1322
  // Normal maintenance: full optimize + bloat check
1258
- yield this.optimize(5, 0, true);
1323
+ result = yield this.optimize(5, 0, true);
1259
1324
  // Track across both passes: `lastOptimizeDidWork` is reset per optimize()
1260
1325
  // call, so a productive first pass followed by a barren bloat retry would
1261
1326
  // otherwise read as unproductive and trigger a spurious backoff.
@@ -1280,7 +1345,7 @@ class VectorDB {
1280
1345
  if (bloatRatio > 2.0 && this.checkDiskPressure() === "ok") {
1281
1346
  (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`);
1282
1347
  yield new Promise((r) => setTimeout(r, 2000));
1283
- yield this.optimize(5, 0, true);
1348
+ result = yield this.optimize(5, 0, true);
1284
1349
  didWork = didWork || this.lastOptimizeDidWork;
1285
1350
  }
1286
1351
  }
@@ -1292,6 +1357,7 @@ class VectorDB {
1292
1357
  yield this.readTableVersion(maintainedTable);
1293
1358
  this.maintainedEpoch = epochSnapshot;
1294
1359
  this.lastMaintenanceMs = Date.now();
1360
+ return result;
1295
1361
  }
1296
1362
  finally {
1297
1363
  this.maintenanceRunning = false;
@@ -1356,9 +1422,9 @@ class VectorDB {
1356
1422
  return false;
1357
1423
  }
1358
1424
  (0, logger_1.log)("vectordb", `Fragment threshold exceeded (${stats.fragmentStats.numSmallFragments} > ${threshold}) — compacting`);
1359
- yield this.optimize(2, 0, true);
1425
+ const result = yield this.optimize(2, 0, true);
1360
1426
  this.noteCompaction(this.lastOptimizeDidWork);
1361
- return true;
1427
+ return result.status === "completed";
1362
1428
  }
1363
1429
  }
1364
1430
  catch (err) {
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.runDoctorOptimize = runDoctorOptimize;
13
+ exports.formatDoctorOptimize = formatDoctorOptimize;
14
+ const compaction_result_1 = require("../store/compaction-result");
15
+ /** A live daemon's failed repair must never become a competing local rewrite. */
16
+ function runDoctorOptimize(send, optimizeInProcess) {
17
+ return __awaiter(this, void 0, void 0, function* () {
18
+ var _a, _b;
19
+ const response = yield send();
20
+ const result = (0, compaction_result_1.parseCompactionResult)(response.compaction);
21
+ if (result) {
22
+ return {
23
+ status: !response.ok && result.status === "completed"
24
+ ? "failed"
25
+ : result.status,
26
+ via: "daemon",
27
+ reason: (_a = result.reason) !== null && _a !== void 0 ? _a : (response.ok ? undefined : String(response.error)),
28
+ result,
29
+ };
30
+ }
31
+ if (response.ok) {
32
+ return {
33
+ status: "unverified",
34
+ via: "daemon",
35
+ reason: "daemon did not report a compaction outcome; restart it with the current gmax version",
36
+ };
37
+ }
38
+ const reason = String((_b = response.error) !== null && _b !== void 0 ? _b : "unknown failure");
39
+ // Socket errors are returned as exact errno codes by sendDaemonCommand.
40
+ // An ENOENT inside a daemon's failure message is not proof it is absent.
41
+ if (reason === "ECONNREFUSED" || reason === "ENOENT") {
42
+ try {
43
+ const local = yield optimizeInProcess();
44
+ return {
45
+ status: local.status,
46
+ via: "in-process",
47
+ reason: local.reason,
48
+ result: local,
49
+ };
50
+ }
51
+ catch (error) {
52
+ return { status: "failed", via: "in-process", reason: String(error) };
53
+ }
54
+ }
55
+ return {
56
+ status: reason.includes("unknown command") ? "skipped" : "failed",
57
+ via: "daemon",
58
+ reason,
59
+ };
60
+ });
61
+ }
62
+ function formatDoctorOptimize(report, agent) {
63
+ var _a;
64
+ const reason = (_a = report.reason) === null || _a === void 0 ? void 0 : _a.replace(/[\r\n\t]/g, " ").slice(0, 512);
65
+ if (agent) {
66
+ const r = report.result;
67
+ return [
68
+ `optimize`,
69
+ `status=${report.status}`,
70
+ `via=${report.via}`,
71
+ ...(r ? [`attempts=${r.attempts}`, `elapsed_ms=${r.elapsedMs}`] : []),
72
+ ...(typeof (r === null || r === void 0 ? void 0 : r.bytesReclaimed) === "number"
73
+ ? [`bytes_reclaimed=${r.bytesReclaimed}`]
74
+ : []),
75
+ ...(reason ? [`reason=${reason}`] : []),
76
+ ].join("\t");
77
+ }
78
+ if (report.status === "completed")
79
+ return `ok Optimize complete (${report.via})`;
80
+ return `${report.status === "failed" ? "FAIL" : "WARN"} Optimize ${report.status} (${report.via}): ${reason !== null && reason !== void 0 ? reason : "no outcome reported"}`;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.41",
3
+ "version": "0.26.43",
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.41",
3
+ "version": "0.26.43",
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",