grepmax 0.12.13 → 0.13.1

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.
@@ -53,6 +53,7 @@ const watcher = __importStar(require("@parcel/watcher"));
53
53
  const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
54
54
  const config_1 = require("../../config");
55
55
  const batch_processor_1 = require("../index/batch-processor");
56
+ const syncer_1 = require("../index/syncer");
56
57
  const watcher_1 = require("../index/watcher");
57
58
  const meta_cache_1 = require("../store/meta-cache");
58
59
  const vector_db_1 = require("../store/vector-db");
@@ -76,6 +77,7 @@ class Daemon {
76
77
  this.idleInterval = null;
77
78
  this.shuttingDown = false;
78
79
  this.pendingOps = new Set();
80
+ this.projectLocks = new Map();
79
81
  }
80
82
  start() {
81
83
  return __awaiter(this, void 0, void 0, function* () {
@@ -168,9 +170,12 @@ class Daemon {
168
170
  conn.end();
169
171
  return;
170
172
  }
171
- (0, ipc_handler_1.handleCommand)(this, cmd).then((resp) => {
172
- conn.write(`${JSON.stringify(resp)}\n`);
173
- conn.end();
173
+ (0, ipc_handler_1.handleCommand)(this, cmd, conn).then((resp) => {
174
+ // null means the handler is managing the connection (streaming)
175
+ if (resp !== null) {
176
+ conn.write(`${JSON.stringify(resp)}\n`);
177
+ conn.end();
178
+ }
174
179
  });
175
180
  });
176
181
  conn.on("error", () => { });
@@ -207,7 +212,6 @@ class Daemon {
207
212
  projectRoot: root,
208
213
  vectorDb: this.vectorDb,
209
214
  metaCache: this.metaCache,
210
- dataDir: config_1.PATHS.globalRoot,
211
215
  onReindex: (files, ms) => {
212
216
  console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
213
217
  // Update project registry so gmax status shows fresh data
@@ -286,6 +290,212 @@ class Daemon {
286
290
  uptime() {
287
291
  return Math.floor((Date.now() - this.startTime) / 1000);
288
292
  }
293
+ /** Reset idle timer — call during long-running operations. */
294
+ resetActivity() {
295
+ this.lastActivity = Date.now();
296
+ }
297
+ // --- Per-project operation serialization ---
298
+ withProjectLock(root, fn) {
299
+ return __awaiter(this, void 0, void 0, function* () {
300
+ var _a;
301
+ const prev = (_a = this.projectLocks.get(root)) !== null && _a !== void 0 ? _a : Promise.resolve();
302
+ let release;
303
+ const next = new Promise((r) => { release = r; });
304
+ this.projectLocks.set(root, next);
305
+ yield prev;
306
+ try {
307
+ return yield fn();
308
+ }
309
+ finally {
310
+ release();
311
+ if (this.projectLocks.get(root) === next) {
312
+ this.projectLocks.delete(root);
313
+ }
314
+ }
315
+ });
316
+ }
317
+ // --- Streaming write operations (IPC) ---
318
+ addProject(root, conn) {
319
+ return __awaiter(this, void 0, void 0, function* () {
320
+ yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
321
+ var _a;
322
+ if (!this.vectorDb || !this.metaCache) {
323
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
324
+ return;
325
+ }
326
+ const ac = new AbortController();
327
+ conn.on("close", () => ac.abort());
328
+ this.vectorDb.pauseMaintenanceLoop();
329
+ let lastProgressTime = 0;
330
+ try {
331
+ const result = yield (0, syncer_1.initialSync)({
332
+ projectRoot: root,
333
+ vectorDb: this.vectorDb,
334
+ metaCache: this.metaCache,
335
+ signal: ac.signal,
336
+ onProgress: (info) => {
337
+ this.resetActivity();
338
+ const now = Date.now();
339
+ if (now - lastProgressTime < 100)
340
+ return;
341
+ lastProgressTime = now;
342
+ (0, ipc_handler_1.writeProgress)(conn, {
343
+ processed: info.processed,
344
+ indexed: info.indexed,
345
+ total: info.total,
346
+ filePath: info.filePath,
347
+ });
348
+ },
349
+ });
350
+ yield this.watchProject(root);
351
+ (0, ipc_handler_1.writeDone)(conn, {
352
+ ok: true,
353
+ processed: result.processed,
354
+ indexed: result.indexed,
355
+ total: result.total,
356
+ failedFiles: result.failedFiles,
357
+ });
358
+ }
359
+ catch (err) {
360
+ const msg = err instanceof Error ? err.message : String(err);
361
+ console.error(`[daemon] addProject failed for ${path.basename(root)}:`, msg);
362
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
363
+ }
364
+ finally {
365
+ (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
366
+ }
367
+ }));
368
+ });
369
+ }
370
+ indexProject(root, conn, opts) {
371
+ return __awaiter(this, void 0, void 0, function* () {
372
+ yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
373
+ var _a;
374
+ if (!this.vectorDb || !this.metaCache) {
375
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
376
+ return;
377
+ }
378
+ // Pause the project's batch processor during full index
379
+ const processor = this.processors.get(root);
380
+ if (processor) {
381
+ yield processor.close();
382
+ this.processors.delete(root);
383
+ }
384
+ const sub = this.subscriptions.get(root);
385
+ if (sub) {
386
+ yield sub.unsubscribe();
387
+ this.subscriptions.delete(root);
388
+ }
389
+ const ac = new AbortController();
390
+ conn.on("close", () => ac.abort());
391
+ this.vectorDb.pauseMaintenanceLoop();
392
+ let lastProgressTime = 0;
393
+ try {
394
+ const result = yield (0, syncer_1.initialSync)({
395
+ projectRoot: root,
396
+ reset: opts.reset,
397
+ dryRun: opts.dryRun,
398
+ vectorDb: this.vectorDb,
399
+ metaCache: this.metaCache,
400
+ signal: ac.signal,
401
+ onProgress: (info) => {
402
+ this.resetActivity();
403
+ const now = Date.now();
404
+ if (now - lastProgressTime < 100)
405
+ return;
406
+ lastProgressTime = now;
407
+ (0, ipc_handler_1.writeProgress)(conn, {
408
+ processed: info.processed,
409
+ indexed: info.indexed,
410
+ total: info.total,
411
+ filePath: info.filePath,
412
+ });
413
+ },
414
+ });
415
+ (0, ipc_handler_1.writeDone)(conn, {
416
+ ok: true,
417
+ processed: result.processed,
418
+ indexed: result.indexed,
419
+ total: result.total,
420
+ failedFiles: result.failedFiles,
421
+ });
422
+ }
423
+ catch (err) {
424
+ const msg = err instanceof Error ? err.message : String(err);
425
+ console.error(`[daemon] indexProject failed for ${path.basename(root)}:`, msg);
426
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
427
+ }
428
+ finally {
429
+ (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
430
+ // Re-enable watcher
431
+ try {
432
+ yield this.watchProject(root);
433
+ }
434
+ catch (err) {
435
+ console.error(`[daemon] Failed to re-watch ${path.basename(root)}:`, err);
436
+ }
437
+ }
438
+ }));
439
+ });
440
+ }
441
+ removeProject(root, conn) {
442
+ return __awaiter(this, void 0, void 0, function* () {
443
+ yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
444
+ if (!this.vectorDb || !this.metaCache) {
445
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
446
+ return;
447
+ }
448
+ try {
449
+ yield this.unwatchProject(root);
450
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
451
+ yield this.vectorDb.deletePathsWithPrefix(rootPrefix);
452
+ const keys = yield this.metaCache.getKeysWithPrefix(rootPrefix);
453
+ for (const key of keys) {
454
+ this.metaCache.delete(key);
455
+ }
456
+ (0, ipc_handler_1.writeDone)(conn, { ok: true });
457
+ }
458
+ catch (err) {
459
+ const msg = err instanceof Error ? err.message : String(err);
460
+ console.error(`[daemon] removeProject failed for ${path.basename(root)}:`, msg);
461
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
462
+ }
463
+ }));
464
+ });
465
+ }
466
+ summarizeProject(root, conn, opts) {
467
+ return __awaiter(this, void 0, void 0, function* () {
468
+ yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
469
+ var _a;
470
+ if (!this.vectorDb) {
471
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
472
+ return;
473
+ }
474
+ const rootPrefix = (_a = opts.pathPrefix) !== null && _a !== void 0 ? _a : (root.endsWith("/") ? root : `${root}/`);
475
+ let lastProgressTime = 0;
476
+ try {
477
+ const result = yield (0, syncer_1.generateSummaries)(this.vectorDb, rootPrefix, (done, total) => {
478
+ this.resetActivity();
479
+ const now = Date.now();
480
+ if (now - lastProgressTime < 100)
481
+ return;
482
+ lastProgressTime = now;
483
+ (0, ipc_handler_1.writeProgress)(conn, { summarized: done, total });
484
+ }, opts.limit);
485
+ (0, ipc_handler_1.writeDone)(conn, {
486
+ ok: true,
487
+ summarized: result.summarized,
488
+ remaining: result.remaining,
489
+ });
490
+ }
491
+ catch (err) {
492
+ const msg = err instanceof Error ? err.message : String(err);
493
+ console.error(`[daemon] summarizeProject failed for ${path.basename(root)}:`, msg);
494
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
495
+ }
496
+ }));
497
+ });
498
+ }
289
499
  shutdown() {
290
500
  return __awaiter(this, void 0, void 0, function* () {
291
501
  var _a, _b, _c;
@@ -9,8 +9,33 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.writeProgress = writeProgress;
13
+ exports.writeDone = writeDone;
12
14
  exports.handleCommand = handleCommand;
13
- function handleCommand(daemon, cmd) {
15
+ /**
16
+ * Write a streaming progress line to the IPC connection.
17
+ */
18
+ function writeProgress(conn, data) {
19
+ if (!conn.writable)
20
+ return;
21
+ conn.write(`${JSON.stringify(Object.assign({ type: "progress" }, data))}\n`);
22
+ }
23
+ /**
24
+ * Write the final streaming done line and end the connection.
25
+ */
26
+ function writeDone(conn, data) {
27
+ if (!conn.writable)
28
+ return;
29
+ conn.write(`${JSON.stringify(Object.assign({ type: "done" }, data))}\n`);
30
+ conn.end();
31
+ }
32
+ /**
33
+ * Handle a single IPC command.
34
+ *
35
+ * Returns a DaemonResponse for simple commands (caller writes + closes).
36
+ * Returns null for streaming commands (handler manages connection lifecycle).
37
+ */
38
+ function handleCommand(daemon, cmd, conn) {
14
39
  return __awaiter(this, void 0, void 0, function* () {
15
40
  try {
16
41
  switch (cmd.cmd) {
@@ -41,6 +66,41 @@ function handleCommand(daemon, cmd) {
41
66
  // Respond before shutting down so the client gets the response
42
67
  setImmediate(() => daemon.shutdown());
43
68
  return { ok: true };
69
+ // --- Streaming commands (daemon manages connection) ---
70
+ case "add": {
71
+ const root = String(cmd.root || "");
72
+ if (!root)
73
+ return { ok: false, error: "missing root" };
74
+ daemon.addProject(root, conn);
75
+ return null;
76
+ }
77
+ case "index": {
78
+ const root = String(cmd.root || "");
79
+ if (!root)
80
+ return { ok: false, error: "missing root" };
81
+ daemon.indexProject(root, conn, {
82
+ reset: !!cmd.reset,
83
+ dryRun: !!cmd.dryRun,
84
+ });
85
+ return null;
86
+ }
87
+ case "remove": {
88
+ const root = String(cmd.root || "");
89
+ if (!root)
90
+ return { ok: false, error: "missing root" };
91
+ daemon.removeProject(root, conn);
92
+ return null;
93
+ }
94
+ case "summarize": {
95
+ const root = String(cmd.root || "");
96
+ if (!root)
97
+ return { ok: false, error: "missing root" };
98
+ daemon.summarizeProject(root, conn, {
99
+ limit: typeof cmd.limit === "number" ? cmd.limit : undefined,
100
+ pathPrefix: typeof cmd.pathPrefix === "string" ? cmd.pathPrefix : undefined,
101
+ });
102
+ return null;
103
+ }
44
104
  default:
45
105
  return { ok: false, error: `unknown command: ${cmd.cmd}` };
46
106
  }
@@ -49,7 +49,6 @@ const config_1 = require("../../config");
49
49
  const cache_check_1 = require("../utils/cache-check");
50
50
  const file_utils_1 = require("../utils/file-utils");
51
51
  const logger_1 = require("../utils/logger");
52
- const lock_1 = require("../utils/lock");
53
52
  const pool_1 = require("../workers/pool");
54
53
  const watcher_batch_1 = require("./watcher-batch");
55
54
  const DEBOUNCE_MS = 2000;
@@ -61,12 +60,10 @@ class ProjectBatchProcessor {
61
60
  this.debounceTimer = null;
62
61
  this.processing = false;
63
62
  this.closed = false;
64
- this.consecutiveLockFailures = 0;
65
63
  this.currentBatchAc = null;
66
64
  this.projectRoot = opts.projectRoot;
67
65
  this.vectorDb = opts.vectorDb;
68
66
  this.metaCache = opts.metaCache;
69
- this.dataDir = opts.dataDir;
70
67
  this.onReindex = opts.onReindex;
71
68
  this.onActivity = opts.onActivity;
72
69
  this.wtag = `watch:${path.basename(opts.projectRoot)}`;
@@ -122,127 +119,114 @@ class ProjectBatchProcessor {
122
119
  const start = Date.now();
123
120
  let reindexed = 0;
124
121
  try {
125
- const lock = yield (0, lock_1.acquireWriterLockWithRetry)(this.dataDir, {
126
- maxRetries: 3,
127
- retryDelayMs: 500,
128
- });
129
- try {
130
- const pool = (0, pool_1.getWorkerPool)();
131
- const deletes = [];
132
- const vectors = [];
133
- const metaUpdates = new Map();
134
- const metaDeletes = [];
135
- const attempted = new Set();
136
- for (const [absPath, event] of batch) {
137
- if (batchAc.signal.aborted)
138
- break;
139
- attempted.add(absPath);
140
- if (event === "unlink") {
141
- deletes.push(absPath);
142
- metaDeletes.push(absPath);
143
- reindexed++;
122
+ // No lock needed daemon is the single writer to LanceDB/MetaCache
123
+ const pool = (0, pool_1.getWorkerPool)();
124
+ const deletes = [];
125
+ const vectors = [];
126
+ const metaUpdates = new Map();
127
+ const metaDeletes = [];
128
+ const attempted = new Set();
129
+ for (const [absPath, event] of batch) {
130
+ if (batchAc.signal.aborted)
131
+ break;
132
+ attempted.add(absPath);
133
+ if (event === "unlink") {
134
+ deletes.push(absPath);
135
+ metaDeletes.push(absPath);
136
+ reindexed++;
137
+ continue;
138
+ }
139
+ // change or add
140
+ try {
141
+ const stats = yield fs.promises.stat(absPath);
142
+ if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
143
+ continue;
144
+ const cached = this.metaCache.get(absPath);
145
+ if ((0, cache_check_1.isFileCached)(cached, stats)) {
144
146
  continue;
145
147
  }
146
- // change or add
147
- try {
148
- const stats = yield fs.promises.stat(absPath);
149
- if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
150
- continue;
151
- const cached = this.metaCache.get(absPath);
152
- if ((0, cache_check_1.isFileCached)(cached, stats)) {
153
- continue;
154
- }
155
- const result = yield pool.processFile({
156
- path: absPath,
157
- absolutePath: absPath,
158
- }, batchAc.signal);
159
- const metaEntry = {
160
- hash: result.hash,
161
- mtimeMs: result.mtimeMs,
162
- size: result.size,
163
- };
164
- if (cached && cached.hash === result.hash) {
165
- metaUpdates.set(absPath, metaEntry);
166
- continue;
167
- }
168
- if (result.shouldDelete) {
169
- deletes.push(absPath);
170
- metaUpdates.set(absPath, metaEntry);
171
- reindexed++;
172
- continue;
173
- }
148
+ const result = yield pool.processFile({
149
+ path: absPath,
150
+ absolutePath: absPath,
151
+ }, batchAc.signal);
152
+ const metaEntry = {
153
+ hash: result.hash,
154
+ mtimeMs: result.mtimeMs,
155
+ size: result.size,
156
+ };
157
+ if (cached && cached.hash === result.hash) {
158
+ metaUpdates.set(absPath, metaEntry);
159
+ continue;
160
+ }
161
+ if (result.shouldDelete) {
174
162
  deletes.push(absPath);
175
- if (result.vectors.length > 0) {
176
- vectors.push(...result.vectors);
177
- }
178
163
  metaUpdates.set(absPath, metaEntry);
179
164
  reindexed++;
165
+ continue;
180
166
  }
181
- catch (err) {
182
- if (batchAc.signal.aborted)
183
- break;
184
- const code = err === null || err === void 0 ? void 0 : err.code;
185
- if (code === "ENOENT") {
186
- deletes.push(absPath);
187
- metaDeletes.push(absPath);
188
- reindexed++;
189
- }
190
- else {
191
- console.error(`[${this.wtag}] Failed to process ${absPath}:`, err);
192
- if (!pool.isHealthy()) {
193
- console.error(`[${this.wtag}] Worker pool unhealthy, aborting batch`);
194
- break;
195
- }
196
- }
197
- }
198
- }
199
- // Requeue files that weren't attempted (aborted or pool unhealthy)
200
- for (const [absPath, event] of batch) {
201
- if (!attempted.has(absPath) && !this.pending.has(absPath)) {
202
- this.pending.set(absPath, event);
167
+ deletes.push(absPath);
168
+ if (result.vectors.length > 0) {
169
+ vectors.push(...result.vectors);
203
170
  }
171
+ metaUpdates.set(absPath, metaEntry);
172
+ reindexed++;
204
173
  }
205
- // Flush to VectorDB: insert first, then delete old (preserving new)
206
- const newIds = vectors.map((v) => v.id);
207
- if (vectors.length > 0) {
208
- yield this.vectorDb.insertBatch(vectors);
209
- }
210
- if (deletes.length > 0) {
211
- if (newIds.length > 0) {
212
- yield this.vectorDb.deletePathsExcludingIds(deletes, newIds);
174
+ catch (err) {
175
+ if (batchAc.signal.aborted)
176
+ break;
177
+ const code = err === null || err === void 0 ? void 0 : err.code;
178
+ if (code === "ENOENT") {
179
+ deletes.push(absPath);
180
+ metaDeletes.push(absPath);
181
+ reindexed++;
213
182
  }
214
183
  else {
215
- yield this.vectorDb.deletePaths(deletes);
184
+ console.error(`[${this.wtag}] Failed to process ${absPath}:`, err);
185
+ if (!pool.isHealthy()) {
186
+ console.error(`[${this.wtag}] Worker pool unhealthy, aborting batch`);
187
+ break;
188
+ }
216
189
  }
217
190
  }
218
- // Update MetaCache
219
- for (const [p, entry] of metaUpdates) {
220
- this.metaCache.put(p, entry);
191
+ }
192
+ // Requeue files that weren't attempted (aborted or pool unhealthy)
193
+ for (const [absPath, event] of batch) {
194
+ if (!attempted.has(absPath) && !this.pending.has(absPath)) {
195
+ this.pending.set(absPath, event);
196
+ }
197
+ }
198
+ // Flush to VectorDB: insert first, then delete old (preserving new)
199
+ const newIds = vectors.map((v) => v.id);
200
+ if (vectors.length > 0) {
201
+ yield this.vectorDb.insertBatch(vectors);
202
+ }
203
+ if (deletes.length > 0) {
204
+ if (newIds.length > 0) {
205
+ yield this.vectorDb.deletePathsExcludingIds(deletes, newIds);
221
206
  }
222
- for (const p of metaDeletes) {
223
- this.metaCache.delete(p);
207
+ else {
208
+ yield this.vectorDb.deletePaths(deletes);
224
209
  }
225
210
  }
226
- finally {
227
- yield lock.release();
211
+ // Update MetaCache
212
+ for (const [p, entry] of metaUpdates) {
213
+ this.metaCache.put(p, entry);
214
+ }
215
+ for (const p of metaDeletes) {
216
+ this.metaCache.delete(p);
228
217
  }
229
218
  const duration = Date.now() - start;
230
219
  if (reindexed > 0) {
231
220
  (_a = this.onReindex) === null || _a === void 0 ? void 0 : _a.call(this, reindexed, duration);
232
221
  }
233
222
  (0, logger_1.log)(this.wtag, `Batch complete: ${batch.size} files, ${reindexed} reindexed (${(duration / 1000).toFixed(1)}s)`);
234
- this.consecutiveLockFailures = 0;
235
223
  for (const absPath of batch.keys()) {
236
224
  this.retryCount.delete(absPath);
237
225
  }
238
226
  }
239
227
  catch (err) {
240
- const isLockError = err instanceof Error && err.message.includes("lock already held");
241
- if (isLockError) {
242
- this.consecutiveLockFailures++;
243
- }
244
228
  console.error(`[${this.wtag}] Batch processing failed:`, err);
245
- const { requeued, dropped, backoffMs } = (0, watcher_batch_1.computeRetryAction)(batch, this.retryCount, MAX_RETRIES, isLockError, this.consecutiveLockFailures, DEBOUNCE_MS);
229
+ const { requeued, dropped, backoffMs } = (0, watcher_batch_1.computeRetryAction)(batch, this.retryCount, MAX_RETRIES, false, 0, DEBOUNCE_MS);
246
230
  for (const [absPath, event] of requeued) {
247
231
  if (!this.pending.has(absPath)) {
248
232
  this.pending.set(absPath, event);