grepmax 0.24.0 → 0.25.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.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +15 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +22 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +86 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/index.js +23 -24
  9. package/dist/commands/list.js +31 -14
  10. package/dist/commands/mcp.js +278 -143
  11. package/dist/commands/peek.js +16 -5
  12. package/dist/commands/repair.js +54 -120
  13. package/dist/commands/search-output.js +30 -9
  14. package/dist/commands/search-run.js +75 -47
  15. package/dist/commands/search-skeletons.js +28 -18
  16. package/dist/commands/search.js +45 -49
  17. package/dist/commands/serve.js +415 -373
  18. package/dist/commands/setup.js +2 -2
  19. package/dist/commands/similar.js +5 -5
  20. package/dist/commands/skeleton.js +67 -41
  21. package/dist/commands/status.js +11 -2
  22. package/dist/commands/summarize.js +6 -0
  23. package/dist/commands/watch.js +102 -38
  24. package/dist/config.js +3 -0
  25. package/dist/lib/daemon/daemon.js +1108 -379
  26. package/dist/lib/daemon/ipc-handler.js +122 -13
  27. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  28. package/dist/lib/daemon/process-manager.js +7 -4
  29. package/dist/lib/daemon/search-handler.js +23 -9
  30. package/dist/lib/daemon/watcher-manager.js +353 -110
  31. package/dist/lib/index/batch-processor.js +231 -67
  32. package/dist/lib/index/embedding-generation.js +109 -0
  33. package/dist/lib/index/embedding-status.js +37 -0
  34. package/dist/lib/index/file-policy.js +273 -0
  35. package/dist/lib/index/ignore-patterns.js +4 -0
  36. package/dist/lib/index/index-config.js +18 -4
  37. package/dist/lib/index/syncer.js +256 -79
  38. package/dist/lib/index/walker.js +66 -86
  39. package/dist/lib/index/watcher-batch.js +9 -0
  40. package/dist/lib/index/watcher.js +129 -3
  41. package/dist/lib/llm/server.js +6 -0
  42. package/dist/lib/search/searcher.js +30 -19
  43. package/dist/lib/store/store-lease.js +473 -0
  44. package/dist/lib/store/vector-db.js +302 -57
  45. package/dist/lib/utils/blocked-roots.js +63 -0
  46. package/dist/lib/utils/cross-project.js +7 -3
  47. package/dist/lib/utils/daemon-client.js +24 -1
  48. package/dist/lib/utils/daemon-launcher.js +38 -13
  49. package/dist/lib/utils/doctor-status.js +76 -0
  50. package/dist/lib/utils/file-utils.js +74 -4
  51. package/dist/lib/utils/keyed-mutex.js +101 -0
  52. package/dist/lib/utils/logger.js +57 -1
  53. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  54. package/dist/lib/utils/operation-coordinator.js +146 -0
  55. package/dist/lib/utils/path-containment.js +106 -0
  56. package/dist/lib/utils/process.js +44 -0
  57. package/dist/lib/utils/project-registry.js +351 -3
  58. package/dist/lib/utils/scope-filter.js +3 -9
  59. package/dist/lib/utils/stale-hint.js +12 -19
  60. package/dist/lib/utils/watcher-launcher.js +5 -1
  61. package/dist/lib/utils/watcher-store.js +2 -2
  62. package/dist/lib/workers/colbert-math.js +15 -12
  63. package/dist/lib/workers/embeddings/colbert.js +3 -2
  64. package/dist/lib/workers/embeddings/granite.js +4 -3
  65. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  66. package/dist/lib/workers/orchestrator.js +39 -8
  67. package/dist/lib/workers/pool.js +146 -82
  68. package/dist/lib/workers/process-child.js +11 -2
  69. package/dist/lib/workers/serialized-handler.js +10 -0
  70. package/dist/lib/workers/worker.js +13 -4
  71. package/mlx-embed-server/server.py +21 -3
  72. package/mlx-embed-server/summarizer.py +8 -0
  73. package/package.json +4 -3
  74. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  75. package/plugins/grepmax/hooks/start.js +3 -170
@@ -51,15 +51,20 @@ const net = __importStar(require("node:net"));
51
51
  const path = __importStar(require("node:path"));
52
52
  const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
53
53
  const config_1 = require("../../config");
54
+ const embedding_generation_1 = require("../index/embedding-generation");
55
+ const embedding_status_1 = require("../index/embedding-status");
54
56
  const index_config_1 = require("../index/index-config");
55
57
  const syncer_1 = require("../index/syncer");
56
58
  const server_1 = require("../llm/server");
57
59
  const meta_cache_1 = require("../store/meta-cache");
60
+ const store_lease_1 = require("../store/store-lease");
58
61
  const vector_db_1 = require("../store/vector-db");
59
62
  const daemon_client_1 = require("../utils/daemon-client");
60
63
  const daemon_launcher_1 = require("../utils/daemon-launcher");
64
+ const keyed_mutex_1 = require("../utils/keyed-mutex");
61
65
  const log_rotate_1 = require("../utils/log-rotate");
62
66
  const logger_1 = require("../utils/logger");
67
+ const operation_coordinator_1 = require("../utils/operation-coordinator");
63
68
  const process_1 = require("../utils/process");
64
69
  const project_registry_1 = require("../utils/project-registry");
65
70
  const watcher_store_1 = require("../utils/watcher-store");
@@ -107,8 +112,14 @@ class Daemon {
107
112
  this.searchers = new Map();
108
113
  this.subscriptions = new Map();
109
114
  this.vectorDb = null;
115
+ this.activeConfig = null;
116
+ this.activeGeneration = null;
117
+ this.workerPool = null;
118
+ this.resources = null;
119
+ this.nextResourceGenerationId = 1;
110
120
  this.metaCache = null;
111
121
  this.server = null;
122
+ this.connections = new Set();
112
123
  this.releaseLock = null;
113
124
  this.lastActivity = Date.now();
114
125
  this.startTime = Date.now();
@@ -123,6 +134,7 @@ class Daemon {
123
134
  this.ready = false;
124
135
  this.processManager = new process_manager_1.ProcessManager({
125
136
  getShuttingDown: () => this.shuttingDown,
137
+ getWorkerPids: () => { var _a; return (_a = this.workerPool) === null || _a === void 0 ? void 0 : _a.getWorkerPids(); },
126
138
  });
127
139
  this.mlxServerManager = new mlx_server_manager_1.MlxServerManager({
128
140
  getShuttingDown: () => this.shuttingDown,
@@ -132,6 +144,7 @@ class Daemon {
132
144
  subscriptions: this.subscriptions,
133
145
  getVectorDb: () => this.vectorDb,
134
146
  getMetaCache: () => this.metaCache,
147
+ getWorkerPool: () => this.workerPool,
135
148
  getShuttingDown: () => this.shuttingDown,
136
149
  touchActivity: () => {
137
150
  this.lastActivity = Date.now();
@@ -139,20 +152,63 @@ class Daemon {
139
152
  evictSearcher: (root) => {
140
153
  this.searchers.delete(root);
141
154
  },
155
+ runProjectOperation: (root, name, signal, fn) => this.withProjectLock(root, signal, () => this.runSharedOperation(name, signal, fn)),
142
156
  });
143
- this.projectLocks = new Map();
157
+ this.projectMutex = new keyed_mutex_1.KeyedMutex();
158
+ this.operations = new operation_coordinator_1.OperationCoordinator();
159
+ this.shutdownPromise = null;
144
160
  // Full-index progress per root while initialSync runs (--reset / initial
145
161
  // index). Presence = a full index is in flight; value drives the partial-
146
162
  // result pending count (Phase 6). Cleared in the indexProject finally.
147
163
  this.indexProgress = new Map();
148
164
  this.shutdownAbortControllers = new Set();
165
+ this.pendingIndexRetryTimers = new Map();
166
+ this.pendingIndexRetryCounts = new Map();
149
167
  this.llmServer = null;
150
168
  }
169
+ assertStartupActive() {
170
+ if (this.shuttingDown)
171
+ throw new operation_coordinator_1.OperationClosedError();
172
+ }
173
+ createWorkerPool(generation, embedMode) {
174
+ return new pool_1.WorkerPool(generation, embedMode);
175
+ }
176
+ createVectorDb(vectorDim, lease) {
177
+ return new vector_db_1.VectorDB(config_1.PATHS.lancedbDir, vectorDim, lease);
178
+ }
179
+ mlxMode(config) {
180
+ if (config.embedMode !== "gpu")
181
+ return "cpu";
182
+ const state = this.mlxServerManager.getStatus().state;
183
+ return state === "owned-ready"
184
+ ? "owned"
185
+ : state === "adopted-ready"
186
+ ? "adopted"
187
+ : "cpu";
188
+ }
189
+ publishResourceGeneration(config, embedding, vectorDb, workerPool, mlx) {
190
+ const resources = Object.freeze({
191
+ id: this.nextResourceGenerationId++,
192
+ config: Object.freeze(Object.assign({}, config)),
193
+ embedding,
194
+ vectorDb,
195
+ workerPool,
196
+ mlx,
197
+ });
198
+ this.activeConfig = resources.config;
199
+ this.activeGeneration = resources.embedding;
200
+ this.vectorDb = resources.vectorDb;
201
+ this.workerPool = resources.workerPool;
202
+ this.resources = resources;
203
+ return resources;
204
+ }
151
205
  start() {
152
206
  return __awaiter(this, void 0, void 0, function* () {
207
+ var _a;
153
208
  process.title = "gmax-daemon";
154
209
  // 0. Singleton enforcement: find and kill ALL stale daemon/worker processes
155
210
  yield this.processManager.killStaleProcesses();
211
+ this.assertStartupActive();
156
212
  // 1. Acquire exclusive lock — kernel-enforced, atomic, auto-released on death
157
213
  fs.mkdirSync(path.dirname(config_1.PATHS.daemonLockFile), { recursive: true });
158
214
  fs.writeFileSync(config_1.PATHS.daemonLockFile, "", { flag: "a" }); // ensure file exists
@@ -169,6 +225,7 @@ class Daemon {
169
225
  this.shutdown().finally(() => process.exit(0));
170
226
  },
171
227
  });
228
+ this.assertStartupActive();
172
229
  (0, logger_1.debug)("daemon", "lock acquired");
173
230
  }
174
231
  catch (err) {
@@ -187,9 +244,11 @@ class Daemon {
187
244
  try {
188
245
  fs.unlinkSync(config_1.PATHS.daemonSocket);
189
246
  }
190
- catch (_a) { }
247
+ catch (_b) { }
191
248
  this.server = net.createServer((conn) => {
192
249
  (0, logger_1.debug)("daemon", "client connected");
250
+ this.connections.add(conn);
251
+ conn.once("close", () => this.connections.delete(conn));
193
252
  let buf = "";
194
253
  conn.on("data", (chunk) => {
195
254
  buf += chunk.toString();
@@ -239,6 +298,7 @@ class Daemon {
239
298
  });
240
299
  this.server.listen(config_1.PATHS.daemonSocket, () => resolve());
241
300
  });
301
+ this.assertStartupActive();
242
302
  // 3. Write PID file AFTER socket is listening — ensures any process that
243
303
  // reads the PID can immediately ping this daemon and get a response.
244
304
  fs.writeFileSync(config_1.PATHS.daemonPidFile, String(process.pid));
@@ -247,6 +307,7 @@ class Daemon {
247
307
  for (const w of existing) {
248
308
  console.log(`[daemon] Taking over from per-project watcher (PID: ${w.pid}, ${path.basename(w.projectRoot)})`);
249
309
  yield (0, process_1.killProcess)(w.pid);
310
+ this.assertStartupActive();
250
311
  (0, watcher_store_1.unregisterWatcher)(w.pid);
251
312
  }
252
313
  // 5. Open shared resources
@@ -254,10 +315,14 @@ class Daemon {
254
315
  fs.mkdirSync(config_1.PATHS.cacheDir, { recursive: true });
255
316
  fs.mkdirSync(config_1.PATHS.lancedbDir, { recursive: true });
256
317
  console.log("[daemon] Opening LanceDB:", config_1.PATHS.lancedbDir);
257
- this.vectorDb = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
258
- this.vectorDb.startMaintenanceLoop();
318
+ this.activeConfig = (0, index_config_1.readGlobalConfig)();
319
+ this.activeGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(this.activeConfig);
320
+ this.vectorDb = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir, this.activeGeneration.vectorDim);
321
+ this.workerPool = this.createWorkerPool(this.activeGeneration, this.activeConfig.embedMode);
322
+ this.vectorDb.startMaintenanceLoop((fn) => this.runSharedOperation("store-maintenance", undefined, () => fn()));
259
323
  console.log("[daemon] Opening MetaCache:", config_1.PATHS.lmdbPath);
260
324
  this.metaCache = new meta_cache_1.MetaCache(config_1.PATHS.lmdbPath);
325
+ this.assertStartupActive();
261
326
  // Resources are open — only now may resource-dependent IPC commands run.
262
327
  this.ready = true;
263
328
  }
@@ -268,11 +333,13 @@ class Daemon {
268
333
  // 6. LLM server manager (constructed, not started — starts on first request)
269
334
  this.llmServer = new server_1.LlmServer();
270
335
  // 6b. MLX embed server — start if GPU mode is active
271
- const globalConfig = (0, index_config_1.readGlobalConfig)();
336
+ const globalConfig = (_a = this.activeConfig) !== null && _a !== void 0 ? _a : (0, index_config_1.readGlobalConfig)();
272
337
  const isAppleSilicon = process.arch === "arm64" && process.platform === "darwin";
273
338
  if (isAppleSilicon && globalConfig.embedMode === "gpu") {
274
339
  yield this.mlxServerManager.ensureMlxServer(globalConfig.mlxModel);
340
+ this.assertStartupActive();
275
341
  }
342
+ this.publishResourceGeneration(globalConfig, this.activeGeneration, this.vectorDb, this.workerPool, this.mlxMode(globalConfig));
276
343
  // 7. Register daemon (only after resources are open)
277
344
  (0, watcher_store_1.registerDaemon)(process.pid);
278
345
  // 8. Subscribe to all registered projects (skip missing directories)
@@ -285,6 +352,7 @@ class Daemon {
285
352
  }
286
353
  try {
287
354
  yield this.watchProject(p.root);
355
+ this.assertStartupActive();
288
356
  }
289
357
  catch (err) {
290
358
  console.error(`[daemon] Failed to watch ${path.basename(p.root)}:`, err);
@@ -297,6 +365,7 @@ class Daemon {
297
365
  // snapshot, so a new project op kicked off after the snapshot would race
298
366
  // with vectorDb.close() and fail with "VectorDB connection is closed".
299
367
  const pending = allProjects.filter((p) => (p.status === "pending" || p.status === "error") &&
368
+ !p.rebuildId &&
300
369
  fs.existsSync(p.root));
301
370
  void (() => __awaiter(this, void 0, void 0, function* () {
302
371
  for (const p of pending) {
@@ -365,15 +434,16 @@ class Daemon {
365
434
  setTimeout(() => {
366
435
  if (this.shuttingDown)
367
436
  return;
368
- void (() => __awaiter(this, void 0, void 0, function* () {
437
+ void this.runSharedOperation("search-warmup", undefined, () => __awaiter(this, void 0, void 0, function* () {
369
438
  const t0 = Date.now();
370
439
  try {
371
440
  if (this.vectorDb) {
372
441
  yield this.vectorDb.ensureTable();
373
442
  yield this.vectorDb.createFTSIndex();
374
443
  }
375
- const { getWorkerPool } = yield Promise.resolve().then(() => __importStar(require("../workers/pool")));
376
- const pool = getWorkerPool();
444
+ const pool = this.workerPool;
445
+ if (!pool)
446
+ throw new Error("worker pool not ready");
377
447
  // Two parallel encodes force the pool to spawn two workers and
378
448
  // warm both (each worker loads Granite + ColBERT lazily on first
379
449
  // encode — once warmed, subsequent encodes are ~13ms).
@@ -386,64 +456,161 @@ class Daemon {
386
456
  catch (err) {
387
457
  console.log(`[daemon] Search warmup failed (non-fatal): ${err}`);
388
458
  }
389
- }))();
459
+ })).catch((err) => {
460
+ if (err instanceof operation_coordinator_1.OperationClosedError)
461
+ return;
462
+ console.log(`[daemon] Search warmup skipped: ${err}`);
463
+ });
390
464
  }, 5000).unref();
391
465
  });
392
466
  }
393
- watchProject(root) {
394
- return __awaiter(this, void 0, void 0, function* () {
395
- return this.watcherManager.watchProject(root);
396
- });
467
+ watchProject(root, signal) {
468
+ return this.withProjectLock(root, signal, () => this.runSharedOperation("watch", signal, () => this.watchProjectWithinOperation(root)));
469
+ }
470
+ watchProjectWithinOperation(root) {
471
+ const project = (0, project_registry_1.getProject)(root);
472
+ if ((project === null || project === void 0 ? void 0 : project.status) === "indexed" &&
473
+ this.activeGeneration &&
474
+ (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration).state ===
475
+ "stale") {
476
+ throw new Error("project embedding generation is stale; run gmax repair --rebuild to rebuild the whole corpus");
477
+ }
478
+ return this.watcherManager.watchProject(root);
479
+ }
480
+ runSharedOperation(name, signal, fn) {
481
+ return this.operations.runShared(name, signal, fn);
482
+ }
483
+ operationStatus() {
484
+ return this.operations.status;
485
+ }
486
+ resourceGenerationId() {
487
+ var _a, _b;
488
+ return (_b = (_a = this.resources) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
489
+ }
490
+ hasUnfinishedRebuild() {
491
+ try {
492
+ return (0, project_registry_1.hasUnfinishedProjectRebuild)();
493
+ }
494
+ catch (_a) {
495
+ return true;
496
+ }
497
+ }
498
+ schedulePendingIndexRetry(root) {
499
+ var _a;
500
+ if (this.shuttingDown || this.pendingIndexRetryTimers.has(root))
501
+ return;
502
+ const attempts = (_a = this.pendingIndexRetryCounts.get(root)) !== null && _a !== void 0 ? _a : 0;
503
+ if (attempts >= 5)
504
+ return;
505
+ this.pendingIndexRetryCounts.set(root, attempts + 1);
506
+ const timer = setTimeout(() => {
507
+ this.pendingIndexRetryTimers.delete(root);
508
+ if (!this.shuttingDown)
509
+ void this.indexPendingProject(root);
510
+ }, 30000);
511
+ timer.unref();
512
+ this.pendingIndexRetryTimers.set(root, timer);
513
+ }
514
+ clearPendingIndexRetry(root) {
515
+ const timer = this.pendingIndexRetryTimers.get(root);
516
+ if (timer)
517
+ clearTimeout(timer);
518
+ this.pendingIndexRetryTimers.delete(root);
519
+ this.pendingIndexRetryCounts.delete(root);
397
520
  }
398
521
  indexPendingProject(root) {
399
522
  return __awaiter(this, void 0, void 0, function* () {
400
- yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
401
- var _a;
402
- // Bail if shutdown raced ahead of us between IIFE iteration and lock
403
- // acquisition otherwise we'd start writing to a DB that shutdown is
404
- // about to close, leaving the project status as "error".
405
- if (this.shuttingDown)
406
- return;
407
- if (!this.vectorDb || !this.metaCache)
408
- return;
409
- const name = path.basename(root);
410
- const start = Date.now();
411
- (0, logger_1.log)("daemon", `indexPendingProject start: ${name} (${root})`);
412
- this.vectorDb.pauseMaintenanceLoop();
413
- try {
414
- const result = yield (0, syncer_1.initialSync)({
415
- projectRoot: root,
416
- vectorDb: this.vectorDb,
417
- metaCache: this.metaCache,
418
- onProgress: () => {
419
- this.resetActivity();
420
- },
421
- });
422
- const proj = (0, project_registry_1.getProject)(root);
423
- if (proj) {
424
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION }));
425
- }
426
- yield this.watchProject(root);
427
- (0, logger_1.log)("daemon", `indexPendingProject done: ${name} ${result.total} files, ${result.indexed} chunks, ${Date.now() - start}ms`);
428
- }
429
- catch (err) {
430
- const msg = err instanceof Error ? err.message : String(err);
431
- console.error(`[daemon] indexPendingProject failed for ${name} after ${Date.now() - start}ms: ${msg}`);
432
- const proj = (0, project_registry_1.getProject)(root);
433
- if (proj) {
434
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { status: "error" }));
435
- }
436
- }
437
- finally {
438
- (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
439
- }
440
- }));
523
+ const ac = new AbortController();
524
+ this.shutdownAbortControllers.add(ac);
525
+ try {
526
+ yield this.withProjectLock(root, ac.signal, () => __awaiter(this, void 0, void 0, function* () {
527
+ return this.runSharedOperation("index-pending", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
528
+ var _a, _b, _c, _d;
529
+ // Bail if shutdown raced ahead of us between iteration and lock
530
+ // acquisition. Starting now would race the store close below.
531
+ if (this.shuttingDown)
532
+ return;
533
+ if (!this.vectorDb || !this.metaCache)
534
+ return;
535
+ const current = (0, project_registry_1.getProject)(root);
536
+ if ((current === null || current === void 0 ? void 0 : current.status) !== "pending" && (current === null || current === void 0 ? void 0 : current.status) !== "error")
537
+ return;
538
+ const name = path.basename(root);
539
+ const start = Date.now();
540
+ (0, logger_1.log)("daemon", `indexPendingProject start: ${name} (${root})`);
541
+ this.vectorDb.pauseMaintenanceLoop();
542
+ try {
543
+ if (this.processors.has(root))
544
+ yield this.unwatchProjectWithinOperation(root);
545
+ const result = yield (0, syncer_1.initialSync)({
546
+ projectRoot: root,
547
+ vectorDb: this.vectorDb,
548
+ metaCache: this.metaCache,
549
+ signal: operationSignal,
550
+ generation: (_a = this.activeGeneration) !== null && _a !== void 0 ? _a : undefined,
551
+ embedMode: (_b = this.activeConfig) === null || _b === void 0 ? void 0 : _b.embedMode,
552
+ workerPool: (_c = this.workerPool) !== null && _c !== void 0 ? _c : undefined,
553
+ onProgress: () => {
554
+ this.resetActivity();
555
+ },
556
+ });
557
+ const prefix = root.endsWith("/") ? root : `${root}/`;
558
+ const chunkCount = yield this.vectorDb.countRowsForPath(prefix);
559
+ const proj = (0, project_registry_1.getProject)(root);
560
+ if (proj) {
561
+ if (result.degraded) {
562
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { status: "pending" }));
563
+ this.schedulePendingIndexRetry(root);
564
+ }
565
+ else {
566
+ this.clearPendingIndexRetry(root);
567
+ (0, project_registry_1.stampProjectFullSync)({
568
+ root,
569
+ name: proj.name,
570
+ generation: result.generation,
571
+ embedMode: result.embedMode,
572
+ chunkCount,
573
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
574
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
575
+ expectedRebuildId: result.registryExpectation.rebuildId,
576
+ });
577
+ }
578
+ }
579
+ (0, logger_1.log)("daemon", `indexPendingProject done: ${name} — ${result.total} files, ${result.indexed} chunks, ${Date.now() - start}ms`);
580
+ }
581
+ catch (err) {
582
+ const msg = err instanceof Error ? err.message : String(err);
583
+ console.error(`[daemon] indexPendingProject failed for ${name} after ${Date.now() - start}ms: ${msg}`);
584
+ const proj = (0, project_registry_1.getProject)(root);
585
+ if (proj && !(err instanceof project_registry_1.ProjectRegistryConflictError)) {
586
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { status: "error" }));
587
+ this.schedulePendingIndexRetry(root);
588
+ }
589
+ }
590
+ finally {
591
+ (_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.resumeMaintenanceLoop();
592
+ if (!this.shuttingDown) {
593
+ try {
594
+ yield this.watchProjectWithinOperation(root);
595
+ }
596
+ catch (err) {
597
+ console.error(`[daemon] Failed to re-watch ${name}:`, err);
598
+ }
599
+ }
600
+ }
601
+ }));
602
+ }));
603
+ }
604
+ finally {
605
+ this.shutdownAbortControllers.delete(ac);
606
+ }
441
607
  });
442
608
  }
443
- unwatchProject(root) {
444
- return __awaiter(this, void 0, void 0, function* () {
445
- return this.watcherManager.unwatchProject(root);
446
- });
609
+ unwatchProject(root, signal) {
610
+ return this.withProjectLock(root, signal, () => this.runSharedOperation("unwatch", signal, () => this.unwatchProjectWithinOperation(root)));
611
+ }
612
+ unwatchProjectWithinOperation(root) {
613
+ return this.watcherManager.unwatchProject(root);
447
614
  }
448
615
  /**
449
616
  * Run a search inside the daemon, reusing the warm VectorDB connection,
@@ -485,16 +652,20 @@ class Daemon {
485
652
  // Search handling lives in search-handler.ts (Phase 12 split). The daemon
486
653
  // supplies its warm VectorDB + watcher/index bookkeeping; the handler runs
487
654
  // the query and assembles the response.
488
- return (0, search_handler_1.handleDaemonSearch)({
489
- vectorDb: this.vectorDb,
490
- processors: this.processors,
491
- indexProgress: this.indexProgress,
492
- searchers: this.searchers,
493
- getIndexState: (root) => this.indexState(root),
494
- touchActivity: () => {
495
- this.lastActivity = Date.now();
496
- },
497
- }, payload, signal);
655
+ return this.runSharedOperation("search", signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
656
+ return (0, search_handler_1.handleDaemonSearch)({
657
+ vectorDb: this.vectorDb,
658
+ processors: this.processors,
659
+ indexProgress: this.indexProgress,
660
+ searchers: this.searchers,
661
+ getIndexState: (root) => this.indexState(root),
662
+ touchActivity: () => {
663
+ this.lastActivity = Date.now();
664
+ },
665
+ generation: this.activeGeneration,
666
+ workerPool: this.workerPool,
667
+ }, payload, operationSignal);
668
+ }));
498
669
  });
499
670
  }
500
671
  listProjects() {
@@ -514,90 +685,250 @@ class Daemon {
514
685
  var _a, _b;
515
686
  return (_b = (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.diskPressure) !== null && _b !== void 0 ? _b : "unknown";
516
687
  }
688
+ getMlxStatus() {
689
+ return this.mlxServerManager.getStatus();
690
+ }
517
691
  /** Reset idle timer — call during long-running operations. */
518
692
  resetActivity() {
519
693
  this.lastActivity = Date.now();
520
694
  }
521
695
  // --- Per-project operation serialization ---
522
- withProjectLock(root, fn) {
696
+ withProjectLock(root, signal, fn) {
523
697
  return __awaiter(this, void 0, void 0, function* () {
524
- var _a;
525
- const prev = (_a = this.projectLocks.get(root)) !== null && _a !== void 0 ? _a : Promise.resolve();
526
- let release;
527
- const next = new Promise((r) => {
528
- release = r;
529
- });
530
- this.projectLocks.set(root, next);
531
- yield prev;
698
+ return this.projectMutex.run(root, signal, fn);
699
+ });
700
+ }
701
+ // --- Streaming write operations (IPC) ---
702
+ activeConfigurationError() {
703
+ const activeConfig = this.activeConfig;
704
+ const activeGeneration = this.activeGeneration;
705
+ if (!activeConfig || !activeGeneration)
706
+ return "daemon resources not ready";
707
+ const currentConfig = (0, index_config_1.readGlobalConfig)();
708
+ let currentGeneration;
709
+ try {
710
+ currentGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(currentConfig);
711
+ }
712
+ catch (error) {
713
+ return error instanceof Error ? error.message : String(error);
714
+ }
715
+ return currentGeneration.fingerprint !== activeGeneration.fingerprint ||
716
+ currentConfig.embedMode !== activeConfig.embedMode ||
717
+ currentConfig.mlxModel !== activeConfig.mlxModel
718
+ ? "daemon configuration is stale; restart the gmax daemon"
719
+ : null;
720
+ }
721
+ ensureProject(root, conn) {
722
+ return __awaiter(this, void 0, void 0, function* () {
723
+ const ac = new AbortController();
724
+ const onClose = () => ac.abort();
725
+ conn.once("close", onClose);
726
+ this.shutdownAbortControllers.add(ac);
532
727
  try {
533
- return yield fn();
728
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("ensure-project", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
729
+ var _a, _b;
730
+ if (operationSignal.aborted || this.shuttingDown)
731
+ return;
732
+ const activeConfig = this.activeConfig;
733
+ const configurationError = this.activeConfigurationError();
734
+ if (!activeConfig || configurationError) {
735
+ (0, ipc_handler_1.writeDone)(conn, {
736
+ ok: false,
737
+ error: configurationError !== null && configurationError !== void 0 ? configurationError : "daemon resources not ready",
738
+ });
739
+ return;
740
+ }
741
+ const project = (0, project_registry_1.getProject)(root);
742
+ if ((project === null || project === void 0 ? void 0 : project.status) === "indexed") {
743
+ if (!this.activeGeneration ||
744
+ (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration)
745
+ .state === "stale") {
746
+ (0, ipc_handler_1.writeDone)(conn, {
747
+ ok: false,
748
+ error: "project embedding generation is stale; run gmax repair --rebuild to rebuild the whole corpus",
749
+ });
750
+ return;
751
+ }
752
+ yield this.watchProjectWithinOperation(root);
753
+ (0, ipc_handler_1.writeDone)(conn, { ok: true, status: "indexed", watched: true });
754
+ return;
755
+ }
756
+ (0, project_registry_1.registerProject)({
757
+ root,
758
+ name: (_a = project === null || project === void 0 ? void 0 : project.name) !== null && _a !== void 0 ? _a : path.basename(root),
759
+ vectorDim: activeConfig.vectorDim,
760
+ modelTier: activeConfig.modelTier,
761
+ embedMode: activeConfig.embedMode,
762
+ lastIndexed: (_b = project === null || project === void 0 ? void 0 : project.lastIndexed) !== null && _b !== void 0 ? _b : "",
763
+ chunkCount: project === null || project === void 0 ? void 0 : project.chunkCount,
764
+ status: "pending",
765
+ chunkerVersion: project === null || project === void 0 ? void 0 : project.chunkerVersion,
766
+ });
767
+ yield this.addProjectLocked(root, conn, operationSignal, project);
768
+ })));
534
769
  }
535
770
  finally {
536
- release();
537
- if (this.projectLocks.get(root) === next) {
538
- this.projectLocks.delete(root);
539
- }
771
+ conn.off("close", onClose);
772
+ this.shutdownAbortControllers.delete(ac);
540
773
  }
541
774
  });
542
775
  }
543
- // --- Streaming write operations (IPC) ---
544
776
  addProject(root, conn) {
545
777
  return __awaiter(this, void 0, void 0, function* () {
546
- yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
547
- var _a;
548
- if (!this.vectorDb || !this.metaCache) {
549
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
550
- return;
551
- }
552
- const ac = new AbortController();
553
- conn.on("close", () => ac.abort());
554
- this.shutdownAbortControllers.add(ac);
555
- this.vectorDb.pauseMaintenanceLoop();
556
- const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
557
- let lastProgressTime = 0;
558
- try {
559
- const result = yield (0, syncer_1.initialSync)({
560
- projectRoot: root,
561
- vectorDb: this.vectorDb,
562
- metaCache: this.metaCache,
563
- signal: ac.signal,
564
- onProgress: (info) => {
565
- this.resetActivity();
566
- const now = Date.now();
567
- if (now - lastProgressTime < 100)
568
- return;
569
- lastProgressTime = now;
570
- (0, ipc_handler_1.writeProgress)(conn, {
571
- processed: info.processed,
572
- indexed: info.indexed,
573
- total: info.total,
574
- filePath: info.filePath,
575
- });
576
- },
577
- });
578
- if (!this.shuttingDown) {
579
- yield this.watchProject(root);
778
+ const ac = new AbortController();
779
+ const onClose = () => ac.abort();
780
+ conn.once("close", onClose);
781
+ this.shutdownAbortControllers.add(ac);
782
+ try {
783
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("add-project", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
784
+ if (operationSignal.aborted || this.shuttingDown)
785
+ return;
786
+ const configurationError = this.activeConfigurationError();
787
+ if (configurationError) {
788
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: configurationError });
789
+ return;
580
790
  }
581
- stopHeartbeat();
582
- (0, ipc_handler_1.writeDone)(conn, {
583
- ok: true,
584
- processed: result.processed,
585
- indexed: result.indexed,
586
- total: result.total,
587
- failedFiles: result.failedFiles,
791
+ yield this.addProjectLocked(root, conn, operationSignal, (0, project_registry_1.getProject)(root));
792
+ })));
793
+ }
794
+ finally {
795
+ conn.off("close", onClose);
796
+ this.shutdownAbortControllers.delete(ac);
797
+ }
798
+ });
799
+ }
800
+ addProjectLocked(root, conn, signal, previousProject) {
801
+ return __awaiter(this, void 0, void 0, function* () {
802
+ var _a, _b, _c, _d;
803
+ if (!this.vectorDb || !this.metaCache || !this.activeConfig) {
804
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
805
+ return;
806
+ }
807
+ if (!(0, project_registry_1.getProject)(root)) {
808
+ (0, project_registry_1.registerProject)({
809
+ root,
810
+ name: path.basename(root),
811
+ vectorDim: this.activeConfig.vectorDim,
812
+ modelTier: this.activeConfig.modelTier,
813
+ embedMode: this.activeConfig.embedMode,
814
+ lastIndexed: "",
815
+ status: "pending",
816
+ });
817
+ }
818
+ this.vectorDb.pauseMaintenanceLoop();
819
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
820
+ let lastProgressTime = 0;
821
+ try {
822
+ const result = yield (0, syncer_1.initialSync)({
823
+ projectRoot: root,
824
+ vectorDb: this.vectorDb,
825
+ metaCache: this.metaCache,
826
+ signal,
827
+ generation: (_a = this.activeGeneration) !== null && _a !== void 0 ? _a : undefined,
828
+ embedMode: this.activeConfig.embedMode,
829
+ workerPool: (_b = this.workerPool) !== null && _b !== void 0 ? _b : undefined,
830
+ onProgress: (info) => {
831
+ this.resetActivity();
832
+ const now = Date.now();
833
+ if (now - lastProgressTime < 100)
834
+ return;
835
+ lastProgressTime = now;
836
+ (0, ipc_handler_1.writeProgress)(conn, {
837
+ processed: info.processed,
838
+ indexed: info.indexed,
839
+ total: info.total,
840
+ filePath: info.filePath,
841
+ });
842
+ },
843
+ });
844
+ if (signal.aborted || this.shuttingDown) {
845
+ const abortError = new Error("Aborted");
846
+ abortError.name = "AbortError";
847
+ throw abortError;
848
+ }
849
+ const prefix = root.endsWith("/") ? root : `${root}/`;
850
+ const chunkCount = yield this.vectorDb.countRowsForPath(prefix);
851
+ const project = (0, project_registry_1.getProject)(root);
852
+ if (result.degraded) {
853
+ if (previousProject)
854
+ (0, project_registry_1.registerProject)(previousProject);
855
+ this.schedulePendingIndexRetry(root);
856
+ }
857
+ else {
858
+ this.clearPendingIndexRetry(root);
859
+ (0, project_registry_1.stampProjectFullSync)({
860
+ root,
861
+ name: (_c = project === null || project === void 0 ? void 0 : project.name) !== null && _c !== void 0 ? _c : path.basename(root),
862
+ generation: result.generation,
863
+ embedMode: result.embedMode,
864
+ chunkCount,
865
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
866
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
867
+ expectedRebuildId: result.registryExpectation.rebuildId,
588
868
  });
589
869
  }
590
- catch (err) {
870
+ yield this.watchProjectWithinOperation(root);
871
+ (0, ipc_handler_1.writeDone)(conn, {
872
+ ok: true,
873
+ processed: result.processed,
874
+ indexed: result.indexed,
875
+ total: result.total,
876
+ failedFiles: result.failedFiles,
877
+ degraded: result.degraded,
878
+ scanErrors: result.scanErrors,
879
+ });
880
+ }
881
+ catch (err) {
882
+ const aborted = signal.aborted || (err === null || err === void 0 ? void 0 : err.name) === "AbortError";
883
+ if (aborted) {
884
+ if (previousProject)
885
+ (0, project_registry_1.registerProject)(previousProject);
886
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "aborted" });
887
+ }
888
+ else {
591
889
  const msg = err instanceof Error ? err.message : String(err);
592
890
  console.error(`[daemon] addProject failed for ${path.basename(root)}:`, msg);
593
- stopHeartbeat();
891
+ const project = (0, project_registry_1.getProject)(root);
892
+ if (project && !(err instanceof project_registry_1.ProjectRegistryConflictError)) {
893
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, project), { status: "error" }));
894
+ }
594
895
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
595
896
  }
596
- finally {
597
- stopHeartbeat();
598
- this.shutdownAbortControllers.delete(ac);
599
- (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
600
- }
897
+ }
898
+ finally {
899
+ stopHeartbeat();
900
+ (_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.resumeMaintenanceLoop();
901
+ }
902
+ });
903
+ }
904
+ projectStats(root) {
905
+ return __awaiter(this, void 0, void 0, function* () {
906
+ return this.runSharedOperation("project-stats", undefined, () => __awaiter(this, void 0, void 0, function* () {
907
+ if (!this.vectorDb)
908
+ throw new Error("daemon resources not ready");
909
+ const project = (0, project_registry_1.getProject)(root);
910
+ if (!project)
911
+ throw new Error("project not registered");
912
+ if (!this.activeConfig)
913
+ throw new Error("daemon resources not ready");
914
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, this.activeConfig);
915
+ const prefix = root.endsWith("/") ? root : `${root}/`;
916
+ const [chunks, files] = yield Promise.all([
917
+ this.vectorDb.countRowsForPath(prefix),
918
+ this.vectorDb.countDistinctFilesForPath(prefix),
919
+ ]);
920
+ return {
921
+ files,
922
+ chunks,
923
+ vectorDim: project.vectorDim,
924
+ modelTier: project.modelTier,
925
+ embedMode: project.embedMode,
926
+ indexedAt: project.lastIndexed,
927
+ watching: this.processors.has(root),
928
+ configuredEmbedding: identity.configured,
929
+ builtEmbedding: identity.built,
930
+ embeddingState: identity.state,
931
+ };
601
932
  }));
602
933
  });
603
934
  }
@@ -612,20 +943,13 @@ class Daemon {
612
943
  */
613
944
  reindexOneProject(root, opts, signal, onProgress) {
614
945
  return __awaiter(this, void 0, void 0, function* () {
946
+ var _a, _b, _c;
615
947
  if (!this.vectorDb || !this.metaCache) {
616
948
  throw new Error("daemon resources not ready");
617
949
  }
618
- // Pause the project's batch processor during full index
619
- const processor = this.processors.get(root);
620
- if (processor) {
621
- yield processor.close();
622
- this.processors.delete(root);
623
- }
624
- const sub = this.subscriptions.get(root);
625
- if (sub) {
626
- yield sub.unsubscribe();
627
- this.subscriptions.delete(root);
628
- }
950
+ // Quiesce the subscription, processor, and any catchup generation before
951
+ // full sync takes deletion authority for this project.
952
+ yield this.unwatchProjectWithinOperation(root);
629
953
  // Mark this root as full-indexing so concurrent searches get a
630
954
  // partial-result footer (Phase 6); seeded at 0/0 until the first tick.
631
955
  this.indexProgress.set(root, { processed: 0, total: 0 });
@@ -637,6 +961,9 @@ class Daemon {
637
961
  vectorDb: this.vectorDb,
638
962
  metaCache: this.metaCache,
639
963
  signal,
964
+ generation: (_a = this.activeGeneration) !== null && _a !== void 0 ? _a : undefined,
965
+ embedMode: (_b = this.activeConfig) === null || _b === void 0 ? void 0 : _b.embedMode,
966
+ workerPool: (_c = this.workerPool) !== null && _c !== void 0 ? _c : undefined,
640
967
  onProgress: (info) => {
641
968
  this.resetActivity();
642
969
  this.indexProgress.set(root, {
@@ -650,9 +977,9 @@ class Daemon {
650
977
  finally {
651
978
  this.indexProgress.delete(root);
652
979
  // Re-enable watcher (skip if shutting down)
653
- if (!this.shuttingDown) {
980
+ if (!this.shuttingDown && opts.rewatch !== false) {
654
981
  try {
655
- yield this.watchProject(root);
982
+ yield this.watchProjectWithinOperation(root);
656
983
  }
657
984
  catch (err) {
658
985
  console.error(`[daemon] Failed to re-watch ${path.basename(root)}:`, err);
@@ -663,52 +990,78 @@ class Daemon {
663
990
  }
664
991
  indexProject(root, conn, opts) {
665
992
  return __awaiter(this, void 0, void 0, function* () {
666
- yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
667
- var _a;
668
- if (!this.vectorDb || !this.metaCache) {
669
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
670
- return;
671
- }
672
- const ac = new AbortController();
673
- conn.on("close", () => ac.abort());
674
- this.shutdownAbortControllers.add(ac);
675
- this.vectorDb.pauseMaintenanceLoop();
676
- const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
677
- let lastProgressTime = 0;
678
- try {
679
- const result = yield this.reindexOneProject(root, opts, ac.signal, (info) => {
680
- const now = Date.now();
681
- if (now - lastProgressTime < 100)
682
- return;
683
- lastProgressTime = now;
684
- (0, ipc_handler_1.writeProgress)(conn, {
685
- processed: info.processed,
686
- indexed: info.indexed,
687
- total: info.total,
688
- filePath: info.filePath,
993
+ const ac = new AbortController();
994
+ const onClose = () => ac.abort();
995
+ conn.once("close", onClose);
996
+ this.shutdownAbortControllers.add(ac);
997
+ try {
998
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("index-project", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
999
+ var _a, _b;
1000
+ if (!this.vectorDb || !this.metaCache) {
1001
+ (0, ipc_handler_1.writeDone)(conn, {
1002
+ ok: false,
1003
+ error: "daemon resources not ready",
689
1004
  });
690
- });
691
- stopHeartbeat();
692
- (0, ipc_handler_1.writeDone)(conn, {
693
- ok: true,
694
- processed: result.processed,
695
- indexed: result.indexed,
696
- total: result.total,
697
- failedFiles: result.failedFiles,
698
- });
699
- }
700
- catch (err) {
701
- const msg = err instanceof Error ? err.message : String(err);
702
- console.error(`[daemon] indexProject failed for ${path.basename(root)}:`, msg);
703
- stopHeartbeat();
704
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
705
- }
706
- finally {
707
- stopHeartbeat();
708
- this.shutdownAbortControllers.delete(ac);
709
- (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
710
- }
711
- }));
1005
+ return;
1006
+ }
1007
+ this.vectorDb.pauseMaintenanceLoop();
1008
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
1009
+ let lastProgressTime = 0;
1010
+ try {
1011
+ const result = yield this.reindexOneProject(root, opts, operationSignal, (info) => {
1012
+ const now = Date.now();
1013
+ if (now - lastProgressTime < 100)
1014
+ return;
1015
+ lastProgressTime = now;
1016
+ (0, ipc_handler_1.writeProgress)(conn, {
1017
+ processed: info.processed,
1018
+ indexed: info.indexed,
1019
+ total: info.total,
1020
+ filePath: info.filePath,
1021
+ });
1022
+ });
1023
+ if (!opts.dryRun && !result.degraded) {
1024
+ const prefix = root.endsWith("/") ? root : `${root}/`;
1025
+ const chunkCount = yield this.vectorDb.countRowsForPath(prefix);
1026
+ const project = (0, project_registry_1.getProject)(root);
1027
+ (0, project_registry_1.stampProjectFullSync)({
1028
+ root,
1029
+ generation: result.generation,
1030
+ embedMode: result.embedMode,
1031
+ chunkCount,
1032
+ chunkerVersion: opts.reset
1033
+ ? config_1.CONFIG.CHUNKER_VERSION
1034
+ : ((_a = project === null || project === void 0 ? void 0 : project.chunkerVersion) !== null && _a !== void 0 ? _a : 1),
1035
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
1036
+ expectedRebuildId: result.registryExpectation.rebuildId,
1037
+ });
1038
+ }
1039
+ (0, ipc_handler_1.writeDone)(conn, {
1040
+ ok: true,
1041
+ processed: result.processed,
1042
+ indexed: result.indexed,
1043
+ total: result.total,
1044
+ failedFiles: result.failedFiles,
1045
+ degraded: result.degraded,
1046
+ scanErrors: result.scanErrors,
1047
+ embeddingFingerprint: result.generation.fingerprint,
1048
+ });
1049
+ }
1050
+ catch (err) {
1051
+ const msg = err instanceof Error ? err.message : String(err);
1052
+ console.error(`[daemon] indexProject failed for ${path.basename(root)}:`, msg);
1053
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
1054
+ }
1055
+ finally {
1056
+ stopHeartbeat();
1057
+ (_b = this.vectorDb) === null || _b === void 0 ? void 0 : _b.resumeMaintenanceLoop();
1058
+ }
1059
+ })));
1060
+ }
1061
+ finally {
1062
+ conn.off("close", onClose);
1063
+ this.shutdownAbortControllers.delete(ac);
1064
+ }
712
1065
  });
713
1066
  }
714
1067
  /**
@@ -724,179 +1077,508 @@ class Daemon {
724
1077
  repairRebuild(conn) {
725
1078
  return __awaiter(this, void 0, void 0, function* () {
726
1079
  var _a;
727
- if (!this.vectorDb || !this.metaCache) {
1080
+ const oldResources = this.resources;
1081
+ const metaCache = this.metaCache;
1082
+ if (!oldResources || !metaCache) {
728
1083
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
729
1084
  return;
730
1085
  }
731
- const ac = new AbortController();
732
- conn.on("close", () => ac.abort());
733
- this.shutdownAbortControllers.add(ac);
734
- this.vectorDb.pauseMaintenanceLoop();
1086
+ const targetConfig = (0, index_config_1.readGlobalConfig)();
1087
+ const targetGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(targetConfig);
1088
+ const clientAbort = new AbortController();
1089
+ let dropCommitted = false;
1090
+ let desiredWatchRoots = [];
1091
+ let watchersRestored = false;
1092
+ let oldPoolDestroyed = false;
1093
+ let oldDbClosed = false;
1094
+ const failClosedOldResources = (lease) => __awaiter(this, void 0, void 0, function* () {
1095
+ this.ready = false;
1096
+ this.resources = null;
1097
+ this.vectorDb = null;
1098
+ this.workerPool = null;
1099
+ if (!oldPoolDestroyed) {
1100
+ yield oldResources.workerPool
1101
+ .destroy({ requireExit: true })
1102
+ .catch(() => { });
1103
+ oldPoolDestroyed = true;
1104
+ }
1105
+ if (!oldDbClosed) {
1106
+ yield oldResources.vectorDb.close().catch(() => { });
1107
+ oldDbClosed = true;
1108
+ }
1109
+ yield lease.release().catch(() => { });
1110
+ });
1111
+ const onClose = () => {
1112
+ if (!dropCommitted)
1113
+ clientAbort.abort(new Error("client disconnected"));
1114
+ };
1115
+ conn.once("close", onClose);
735
1116
  const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
736
- // Reindex every project the daemon would normally index on startup.
737
- const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed" || p.status === "pending");
738
- const globalConfig = (0, index_config_1.readGlobalConfig)();
739
- let rebuilt = 0;
740
- let totalIndexed = 0;
741
- let lastProgressTime = 0;
1117
+ const throwIfPreDropCancelled = (operationSignal) => {
1118
+ if (operationSignal.aborted)
1119
+ throw operationSignal.reason;
1120
+ if (!dropCommitted && clientAbort.signal.aborted) {
1121
+ throw clientAbort.signal.reason;
1122
+ }
1123
+ };
742
1124
  try {
743
- // Drop the shared table recreated lazily at the configured width on the
744
- // first insert of the reindex below.
745
- (0, ipc_handler_1.writeProgress)(conn, { phase: "drop", projectsTotal: projects.length });
746
- yield this.vectorDb.drop();
747
- for (const p of projects) {
748
- if (ac.signal.aborted)
749
- break;
750
- const name = p.name || path.basename(p.root);
751
- yield this.withProjectLock(p.root, () => __awaiter(this, void 0, void 0, function* () {
752
- const result = yield this.reindexOneProject(p.root, { reset: true }, ac.signal, (info) => {
753
- const now = Date.now();
754
- if (now - lastProgressTime < 100)
755
- return;
756
- lastProgressTime = now;
1125
+ const result = yield this.operations.runExclusive("repair", () => __awaiter(this, void 0, void 0, function* () {
1126
+ desiredWatchRoots = yield this.watcherManager.quiesceAll();
1127
+ }), (operationSignal) => __awaiter(this, void 0, void 0, function* () {
1128
+ throwIfPreDropCancelled(operationSignal);
1129
+ oldResources.vectorDb.pauseMaintenanceLoop();
1130
+ this.searchers.clear();
1131
+ (0, ipc_handler_1.writeProgress)(conn, {
1132
+ phase: "lease",
1133
+ message: "waiting for exclusive store ownership",
1134
+ });
1135
+ let exclusiveLease = null;
1136
+ try {
1137
+ exclusiveLease = yield oldResources.vectorDb.upgradeStoreLease(AbortSignal.any([operationSignal, clientAbort.signal]));
1138
+ throwIfPreDropCancelled(operationSignal);
1139
+ }
1140
+ catch (error) {
1141
+ if (exclusiveLease) {
1142
+ try {
1143
+ yield oldResources.vectorDb.downgradeStoreLease();
1144
+ }
1145
+ catch (downgradeError) {
1146
+ yield failClosedOldResources(exclusiveLease);
1147
+ throw new Error(`Failed to restore shared store ownership: ${String(downgradeError)}`);
1148
+ }
1149
+ }
1150
+ oldResources.vectorDb.resumeMaintenanceLoop();
1151
+ throw error;
1152
+ }
1153
+ if (!exclusiveLease) {
1154
+ throw new Error("Exclusive store lease acquisition returned no lease");
1155
+ }
1156
+ let reservation;
1157
+ try {
1158
+ reservation = (0, project_registry_1.reserveProjectsForRebuild)(targetGeneration);
1159
+ }
1160
+ catch (error) {
1161
+ try {
1162
+ yield oldResources.vectorDb.downgradeStoreLease();
1163
+ }
1164
+ catch (downgradeError) {
1165
+ yield failClosedOldResources(exclusiveLease);
1166
+ throw new Error(`Failed to restore shared store ownership: ${String(downgradeError)}`);
1167
+ }
1168
+ oldResources.vectorDb.resumeMaintenanceLoop();
1169
+ throw error;
1170
+ }
1171
+ let targetDb = null;
1172
+ let targetPool = null;
1173
+ let published = false;
1174
+ try {
1175
+ (0, ipc_handler_1.writeProgress)(conn, {
1176
+ phase: "prepare",
1177
+ rebuildId: reservation.rebuildId,
1178
+ projects: reservation.reserved.length,
1179
+ });
1180
+ yield oldResources.workerPool.destroy({ requireExit: true });
1181
+ oldPoolDestroyed = true;
1182
+ if (oldResources.mlx === "owned") {
1183
+ yield this.mlxServerManager.stopMlxServer();
1184
+ }
1185
+ throwIfPreDropCancelled(operationSignal);
1186
+ yield oldResources.vectorDb.close({
1187
+ releaseLease: false,
1188
+ requireClosed: true,
1189
+ });
1190
+ oldDbClosed = true;
1191
+ throwIfPreDropCancelled(operationSignal);
1192
+ targetDb = this.createVectorDb(targetGeneration.vectorDim, exclusiveLease);
1193
+ (0, project_registry_1.markProjectRebuildDropping)(reservation);
1194
+ try {
1195
+ yield targetDb.drop();
1196
+ dropCommitted = true;
1197
+ conn.off("close", onClose);
1198
+ }
1199
+ catch (error) {
1200
+ // LanceDB does not expose a commit token. Inspect physical state:
1201
+ // an absent table means the destructive commit happened; an
1202
+ // unreadable state is treated as uncertain and therefore post-drop.
1203
+ try {
1204
+ dropCommitted = (yield targetDb.getSchemaVectorDim()) === null;
1205
+ }
1206
+ catch (_a) {
1207
+ dropCommitted = true;
1208
+ }
1209
+ if (dropCommitted)
1210
+ conn.off("close", onClose);
1211
+ throw error;
1212
+ }
1213
+ (0, ipc_handler_1.writeProgress)(conn, {
1214
+ phase: "schema",
1215
+ message: "creating target table",
1216
+ });
1217
+ yield targetDb.ensureTable();
1218
+ const physicalDim = yield targetDb.getSchemaVectorDim();
1219
+ if (physicalDim !== targetGeneration.vectorDim) {
1220
+ throw new Error(`rebuilt table dimension ${physicalDim !== null && physicalDim !== void 0 ? physicalDim : "missing"} does not match target ${targetGeneration.vectorDim}`);
1221
+ }
1222
+ if (targetConfig.embedMode === "gpu") {
1223
+ yield this.mlxServerManager.ensureMlxServer(targetGeneration.mlxModel);
1224
+ }
1225
+ const targetMlx = this.mlxMode(targetConfig);
1226
+ targetPool = this.createWorkerPool(targetGeneration, targetConfig.embedMode);
1227
+ this.publishResourceGeneration(targetConfig, targetGeneration, targetDb, targetPool, targetMlx);
1228
+ published = true;
1229
+ let completed = 0;
1230
+ const failures = [];
1231
+ for (const project of reservation.reserved) {
1232
+ if (operationSignal.aborted)
1233
+ throw operationSignal.reason;
757
1234
  (0, ipc_handler_1.writeProgress)(conn, {
758
- phase: "reindex",
759
- project: name,
760
- projectsDone: rebuilt,
761
- projectsTotal: projects.length,
762
- processed: info.processed,
763
- indexed: info.indexed,
764
- total: info.total,
765
- filePath: info.filePath,
1235
+ phase: "index",
1236
+ root: project.root,
1237
+ project: project.name,
1238
+ completed,
1239
+ totalProjects: reservation.reserved.length,
766
1240
  });
767
- });
768
- totalIndexed += result.indexed;
769
- const proj = (0, project_registry_1.getProject)(p.root);
770
- if (proj) {
771
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { vectorDim: globalConfig.vectorDim, modelTier: globalConfig.modelTier, embedMode: globalConfig.embedMode, lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION }));
1241
+ try {
1242
+ const sync = yield this.reindexOneProject(project.root, { reset: true, rewatch: false }, operationSignal, (info) => (0, ipc_handler_1.writeProgress)(conn, {
1243
+ phase: "index",
1244
+ root: project.root,
1245
+ project: project.name,
1246
+ processed: info.processed,
1247
+ indexed: info.indexed,
1248
+ total: info.total,
1249
+ filePath: info.filePath,
1250
+ }));
1251
+ if (sync.degraded) {
1252
+ throw new Error(`degraded scan (${sync.failedFiles} failed files)`);
1253
+ }
1254
+ const prefix = project.root.endsWith("/")
1255
+ ? project.root
1256
+ : `${project.root}/`;
1257
+ const chunkCount = yield targetDb.countRowsForPath(prefix);
1258
+ (0, project_registry_1.stampProjectFullSync)({
1259
+ root: project.root,
1260
+ name: project.name,
1261
+ generation: sync.generation,
1262
+ embedMode: sync.embedMode,
1263
+ chunkCount,
1264
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
1265
+ expectedFingerprint: targetGeneration.fingerprint,
1266
+ expectedRebuildId: reservation.rebuildId,
1267
+ });
1268
+ completed++;
1269
+ }
1270
+ catch (error) {
1271
+ failures.push({
1272
+ root: project.root,
1273
+ error: error instanceof Error ? error.message : String(error),
1274
+ });
1275
+ }
772
1276
  }
773
- }));
774
- rebuilt++;
1277
+ yield targetDb.downgradeStoreLease();
1278
+ targetDb.startMaintenanceLoop((fn) => this.runSharedOperation("store-maintenance", undefined, () => fn()));
1279
+ const currentGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)((0, index_config_1.readGlobalConfig)());
1280
+ const configChanged = currentGeneration.fingerprint !== targetGeneration.fingerprint;
1281
+ if (failures.length === 0) {
1282
+ (0, project_registry_1.completeProjectRebuild)(reservation.rebuildId);
1283
+ }
1284
+ return {
1285
+ completed,
1286
+ total: reservation.reserved.length,
1287
+ failures,
1288
+ configChanged,
1289
+ generation: targetGeneration.fingerprint,
1290
+ };
1291
+ }
1292
+ catch (error) {
1293
+ if (!dropCommitted) {
1294
+ this.ready = false;
1295
+ this.resources = null;
1296
+ let recoveryError;
1297
+ yield (targetPool === null || targetPool === void 0 ? void 0 : targetPool.destroy({ requireExit: true }).catch((cause) => {
1298
+ recoveryError !== null && recoveryError !== void 0 ? recoveryError : (recoveryError = cause);
1299
+ }));
1300
+ if (targetDb) {
1301
+ yield targetDb
1302
+ .close({ releaseLease: false, requireClosed: true })
1303
+ .catch((cause) => {
1304
+ recoveryError !== null && recoveryError !== void 0 ? recoveryError : (recoveryError = cause);
1305
+ });
1306
+ }
1307
+ try {
1308
+ (0, project_registry_1.restoreProjectsAfterRebuild)(reservation);
1309
+ }
1310
+ catch (cause) {
1311
+ recoveryError !== null && recoveryError !== void 0 ? recoveryError : (recoveryError = cause);
1312
+ }
1313
+ if (!recoveryError && !oldPoolDestroyed) {
1314
+ try {
1315
+ yield oldResources.vectorDb.downgradeStoreLease();
1316
+ oldResources.vectorDb.resumeMaintenanceLoop();
1317
+ this.resources = oldResources;
1318
+ this.activeConfig = oldResources.config;
1319
+ this.activeGeneration = oldResources.embedding;
1320
+ this.vectorDb = oldResources.vectorDb;
1321
+ this.workerPool = oldResources.workerPool;
1322
+ this.ready = true;
1323
+ }
1324
+ catch (cause) {
1325
+ recoveryError = cause;
1326
+ }
1327
+ }
1328
+ else if (!recoveryError) {
1329
+ let restoredDb = null;
1330
+ let restoredPool = null;
1331
+ try {
1332
+ restoredDb = oldDbClosed
1333
+ ? this.createVectorDb(oldResources.embedding.vectorDim, exclusiveLease)
1334
+ : oldResources.vectorDb;
1335
+ if (oldResources.config.embedMode === "gpu" &&
1336
+ oldResources.mlx === "owned") {
1337
+ yield this.mlxServerManager.ensureMlxServer(oldResources.embedding.mlxModel);
1338
+ }
1339
+ restoredPool = this.createWorkerPool(oldResources.embedding, oldResources.config.embedMode);
1340
+ yield restoredDb.downgradeStoreLease();
1341
+ restoredDb.startMaintenanceLoop((fn) => this.runSharedOperation("store-maintenance", undefined, () => fn()));
1342
+ this.publishResourceGeneration(oldResources.config, oldResources.embedding, restoredDb, restoredPool, this.mlxMode(oldResources.config));
1343
+ this.ready = true;
1344
+ }
1345
+ catch (cause) {
1346
+ recoveryError = cause;
1347
+ yield (restoredPool === null || restoredPool === void 0 ? void 0 : restoredPool.destroy({ requireExit: true }).catch(() => { }));
1348
+ if (restoredDb) {
1349
+ yield restoredDb.close().catch(() => { });
1350
+ }
1351
+ else {
1352
+ yield exclusiveLease.release().catch(() => { });
1353
+ }
1354
+ }
1355
+ }
1356
+ if (recoveryError) {
1357
+ if (!oldPoolDestroyed) {
1358
+ yield oldResources.workerPool
1359
+ .destroy({ requireExit: true })
1360
+ .catch((cause) => {
1361
+ recoveryError = new Error(`Pre-drop rebuild pool cleanup failed: ${String(recoveryError)}; ${String(cause)}`);
1362
+ });
1363
+ oldPoolDestroyed = true;
1364
+ }
1365
+ if (!oldDbClosed) {
1366
+ yield oldResources.vectorDb.close().catch((cause) => {
1367
+ recoveryError = new Error(`Pre-drop rebuild DB cleanup failed: ${String(recoveryError)}; ${String(cause)}`);
1368
+ });
1369
+ oldDbClosed = true;
1370
+ }
1371
+ yield exclusiveLease.release().catch((cause) => {
1372
+ recoveryError = new Error(`Pre-drop rebuild cleanup failed: ${String(recoveryError)}; ${String(cause)}`);
1373
+ });
1374
+ this.resources = null;
1375
+ this.vectorDb = null;
1376
+ this.workerPool = null;
1377
+ this.ready = false;
1378
+ throw new Error(`Pre-drop rebuild recovery failed: ${String(error)}; ${String(recoveryError)}`);
1379
+ }
1380
+ }
1381
+ else {
1382
+ this.ready = false;
1383
+ this.resources = null;
1384
+ this.activeConfig = targetConfig;
1385
+ this.activeGeneration = targetGeneration;
1386
+ this.vectorDb = targetDb;
1387
+ this.workerPool = published ? targetPool : null;
1388
+ if (targetDb) {
1389
+ yield targetDb.downgradeStoreLease().catch(() => { });
1390
+ }
1391
+ else {
1392
+ yield exclusiveLease.release().catch(() => { });
1393
+ }
1394
+ }
1395
+ throw error;
1396
+ }
1397
+ }));
1398
+ for (const root of desiredWatchRoots) {
1399
+ if (((_a = (0, project_registry_1.getProject)(root)) === null || _a === void 0 ? void 0 : _a.status) !== "indexed")
1400
+ continue;
1401
+ try {
1402
+ yield this.watcherManager.watchProject(root, { catchup: false });
1403
+ }
1404
+ catch (error) {
1405
+ result.failures.push({
1406
+ root,
1407
+ error: `watch restore failed: ${error instanceof Error ? error.message : String(error)}`,
1408
+ });
1409
+ }
775
1410
  }
776
- stopHeartbeat();
777
- (0, ipc_handler_1.writeDone)(conn, {
778
- ok: true,
779
- projects: rebuilt,
780
- projectsTotal: projects.length,
781
- indexed: totalIndexed,
782
- });
1411
+ watchersRestored = true;
1412
+ try {
1413
+ yield this.watcherManager.catchupAll(desiredWatchRoots);
1414
+ }
1415
+ catch (error) {
1416
+ result.failures.push({
1417
+ root: "*",
1418
+ error: `watch catchup failed: ${error instanceof Error ? error.message : String(error)}`,
1419
+ });
1420
+ }
1421
+ (0, ipc_handler_1.writeDone)(conn, Object.assign(Object.assign({ ok: result.failures.length === 0 }, result), (result.configChanged
1422
+ ? {
1423
+ warning: "configured embedding changed during rebuild; another rebuild is required",
1424
+ }
1425
+ : {})));
783
1426
  }
784
- catch (err) {
785
- const msg = err instanceof Error ? err.message : String(err);
786
- console.error("[daemon] repairRebuild failed:", msg);
787
- stopHeartbeat();
788
- (0, ipc_handler_1.writeDone)(conn, {
789
- ok: false,
790
- error: msg,
791
- projects: rebuilt,
792
- indexed: totalIndexed,
793
- });
1427
+ catch (error) {
1428
+ let reportedError = error;
1429
+ if (!dropCommitted && !watchersRestored && this.ready && this.resources) {
1430
+ try {
1431
+ yield this.watcherManager.resumeAll(desiredWatchRoots, {
1432
+ catchup: false,
1433
+ });
1434
+ watchersRestored = true;
1435
+ yield this.watcherManager.catchupAll(desiredWatchRoots);
1436
+ }
1437
+ catch (watchError) {
1438
+ this.ready = false;
1439
+ this.resources = null;
1440
+ reportedError = new Error(`Pre-drop rebuild watcher restoration failed: ${String(error)}; ${String(watchError)}`);
1441
+ }
1442
+ }
1443
+ const details = reportedError instanceof store_lease_1.StoreLeaseTimeoutError
1444
+ ? { blockers: reportedError.blockers }
1445
+ : {};
1446
+ (0, ipc_handler_1.writeDone)(conn, Object.assign({ ok: false, error: reportedError instanceof Error
1447
+ ? reportedError.message
1448
+ : String(reportedError), degraded: dropCommitted }, details));
794
1449
  }
795
1450
  finally {
1451
+ conn.off("close", onClose);
796
1452
  stopHeartbeat();
797
- this.shutdownAbortControllers.delete(ac);
798
- (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
1453
+ if (!this.ready) {
1454
+ setImmediate(() => void this.shutdown());
1455
+ }
799
1456
  }
800
1457
  });
801
1458
  }
802
1459
  removeProject(root, conn) {
803
1460
  return __awaiter(this, void 0, void 0, function* () {
804
- yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
805
- if (!this.vectorDb || !this.metaCache) {
806
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
807
- return;
808
- }
809
- const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
810
- try {
811
- yield this.unwatchProject(root);
812
- const rootPrefix = root.endsWith("/") ? root : `${root}/`;
813
- yield this.vectorDb.deletePathsWithPrefix(rootPrefix);
814
- const keys = yield this.metaCache.getKeysWithPrefix(rootPrefix);
815
- for (const key of keys) {
816
- this.metaCache.delete(key);
1461
+ const ac = new AbortController();
1462
+ const onClose = () => ac.abort();
1463
+ conn.once("close", onClose);
1464
+ this.shutdownAbortControllers.add(ac);
1465
+ try {
1466
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("remove-project", ac.signal, () => __awaiter(this, void 0, void 0, function* () {
1467
+ if (!this.vectorDb || !this.metaCache) {
1468
+ (0, ipc_handler_1.writeDone)(conn, {
1469
+ ok: false,
1470
+ error: "daemon resources not ready",
1471
+ });
1472
+ return;
817
1473
  }
818
- stopHeartbeat();
819
- (0, ipc_handler_1.writeDone)(conn, { ok: true });
820
- }
821
- catch (err) {
822
- const msg = err instanceof Error ? err.message : String(err);
823
- console.error(`[daemon] removeProject failed for ${path.basename(root)}:`, msg);
824
- stopHeartbeat();
825
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
826
- }
827
- finally {
828
- stopHeartbeat();
829
- }
830
- }));
1474
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
1475
+ try {
1476
+ yield this.unwatchProjectWithinOperation(root);
1477
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1478
+ yield this.vectorDb.deletePathsWithPrefix(rootPrefix);
1479
+ const keys = yield this.metaCache.getKeysWithPrefix(rootPrefix);
1480
+ for (const key of keys)
1481
+ this.metaCache.delete(key);
1482
+ (0, ipc_handler_1.writeDone)(conn, { ok: true });
1483
+ }
1484
+ catch (err) {
1485
+ const msg = err instanceof Error ? err.message : String(err);
1486
+ console.error(`[daemon] removeProject failed for ${path.basename(root)}:`, msg);
1487
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
1488
+ }
1489
+ finally {
1490
+ stopHeartbeat();
1491
+ }
1492
+ })));
1493
+ }
1494
+ finally {
1495
+ conn.off("close", onClose);
1496
+ this.shutdownAbortControllers.delete(ac);
1497
+ }
831
1498
  });
832
1499
  }
833
1500
  summarizeProject(root, conn, opts) {
834
1501
  return __awaiter(this, void 0, void 0, function* () {
835
- yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
836
- var _a;
837
- if (!this.vectorDb) {
838
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
839
- return;
840
- }
841
- const rootPrefix = (_a = opts.pathPrefix) !== null && _a !== void 0 ? _a : (root.endsWith("/") ? root : `${root}/`);
842
- const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
843
- let lastProgressTime = 0;
1502
+ const ac = new AbortController();
1503
+ const onClose = () => ac.abort();
1504
+ conn.once("close", onClose);
1505
+ this.shutdownAbortControllers.add(ac);
1506
+ try {
1507
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("summarize-project", ac.signal, () => __awaiter(this, void 0, void 0, function* () {
1508
+ var _a;
1509
+ if (!this.vectorDb) {
1510
+ (0, ipc_handler_1.writeDone)(conn, {
1511
+ ok: false,
1512
+ error: "daemon resources not ready",
1513
+ });
1514
+ return;
1515
+ }
1516
+ const rootPrefix = (_a = opts.pathPrefix) !== null && _a !== void 0 ? _a : (root.endsWith("/") ? root : `${root}/`);
1517
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
1518
+ let lastProgressTime = 0;
1519
+ try {
1520
+ const result = yield (0, syncer_1.generateSummaries)(this.vectorDb, rootPrefix, (done, total) => {
1521
+ this.resetActivity();
1522
+ const now = Date.now();
1523
+ if (now - lastProgressTime < 100)
1524
+ return;
1525
+ lastProgressTime = now;
1526
+ (0, ipc_handler_1.writeProgress)(conn, { summarized: done, total });
1527
+ }, opts.limit);
1528
+ (0, ipc_handler_1.writeDone)(conn, {
1529
+ ok: true,
1530
+ summarized: result.summarized,
1531
+ remaining: result.remaining,
1532
+ });
1533
+ }
1534
+ catch (err) {
1535
+ const msg = err instanceof Error ? err.message : String(err);
1536
+ console.error(`[daemon] summarizeProject failed for ${path.basename(root)}:`, msg);
1537
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
1538
+ }
1539
+ finally {
1540
+ stopHeartbeat();
1541
+ }
1542
+ })));
1543
+ }
1544
+ finally {
1545
+ conn.off("close", onClose);
1546
+ this.shutdownAbortControllers.delete(ac);
1547
+ }
1548
+ });
1549
+ }
1550
+ // --- LLM server management ---
1551
+ llmStart() {
1552
+ return __awaiter(this, void 0, void 0, function* () {
1553
+ return this.runSharedOperation("llm-start", undefined, () => __awaiter(this, void 0, void 0, function* () {
1554
+ if (!this.llmServer)
1555
+ return { ok: false, error: "daemon not initialized" };
844
1556
  try {
845
- const result = yield (0, syncer_1.generateSummaries)(this.vectorDb, rootPrefix, (done, total) => {
846
- this.resetActivity();
847
- const now = Date.now();
848
- if (now - lastProgressTime < 100)
849
- return;
850
- lastProgressTime = now;
851
- (0, ipc_handler_1.writeProgress)(conn, { summarized: done, total });
852
- }, opts.limit);
853
- stopHeartbeat();
854
- (0, ipc_handler_1.writeDone)(conn, {
855
- ok: true,
856
- summarized: result.summarized,
857
- remaining: result.remaining,
858
- });
1557
+ yield this.llmServer.start();
1558
+ this.resetActivity();
1559
+ return Object.assign({ ok: true }, this.llmServer.getStatus());
859
1560
  }
860
1561
  catch (err) {
861
1562
  const msg = err instanceof Error ? err.message : String(err);
862
- console.error(`[daemon] summarizeProject failed for ${path.basename(root)}:`, msg);
863
- stopHeartbeat();
864
- (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
865
- }
866
- finally {
867
- stopHeartbeat();
1563
+ return { ok: false, error: msg };
868
1564
  }
869
1565
  }));
870
1566
  });
871
1567
  }
872
- // --- LLM server management ---
873
- llmStart() {
874
- return __awaiter(this, void 0, void 0, function* () {
875
- if (!this.llmServer)
876
- return { ok: false, error: "daemon not initialized" };
877
- try {
878
- yield this.llmServer.start();
879
- this.resetActivity();
880
- return Object.assign({ ok: true }, this.llmServer.getStatus());
881
- }
882
- catch (err) {
883
- const msg = err instanceof Error ? err.message : String(err);
884
- return { ok: false, error: msg };
885
- }
886
- });
887
- }
888
1568
  llmStop() {
889
1569
  return __awaiter(this, void 0, void 0, function* () {
890
- if (!this.llmServer)
891
- return { ok: false, error: "daemon not initialized" };
892
- try {
893
- yield this.llmServer.stop();
894
- return { ok: true };
895
- }
896
- catch (err) {
897
- const msg = err instanceof Error ? err.message : String(err);
898
- return { ok: false, error: msg };
899
- }
1570
+ return this.runSharedOperation("llm-stop", undefined, () => __awaiter(this, void 0, void 0, function* () {
1571
+ if (!this.llmServer)
1572
+ return { ok: false, error: "daemon not initialized" };
1573
+ try {
1574
+ yield this.llmServer.stop();
1575
+ return { ok: true };
1576
+ }
1577
+ catch (err) {
1578
+ const msg = err instanceof Error ? err.message : String(err);
1579
+ return { ok: false, error: msg };
1580
+ }
1581
+ }));
900
1582
  });
901
1583
  }
902
1584
  llmStatus() {
@@ -910,20 +1592,22 @@ class Daemon {
910
1592
  }
911
1593
  reviewCommit(root, commitRef) {
912
1594
  return __awaiter(this, void 0, void 0, function* () {
913
- this.resetActivity();
914
- try {
915
- if (!this.llmServer) {
916
- console.log("[review] daemon not initialized, skipping");
917
- return;
1595
+ return this.runSharedOperation("review", undefined, () => __awaiter(this, void 0, void 0, function* () {
1596
+ this.resetActivity();
1597
+ try {
1598
+ if (!this.llmServer) {
1599
+ console.log("[review] daemon not initialized, skipping");
1600
+ return;
1601
+ }
1602
+ yield this.llmServer.ensure();
1603
+ const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../llm/review")));
1604
+ const result = yield reviewCommit({ commitRef, projectRoot: root });
1605
+ console.log(`[review] ${result.commit} — ${result.findingCount} finding(s) in ${result.duration}s`);
918
1606
  }
919
- yield this.llmServer.ensure();
920
- const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../llm/review")));
921
- const result = yield reviewCommit({ commitRef, projectRoot: root });
922
- console.log(`[review] ${result.commit} — ${result.findingCount} finding(s) in ${result.duration}s`);
923
- }
924
- catch (err) {
925
- console.error(`[review] failed: ${err instanceof Error ? err.message : String(err)}`);
926
- }
1607
+ catch (err) {
1608
+ console.error(`[review] failed: ${err instanceof Error ? err.message : String(err)}`);
1609
+ }
1610
+ }));
927
1611
  });
928
1612
  }
929
1613
  /**
@@ -945,7 +1629,7 @@ class Daemon {
945
1629
  // Defer while busy; we'll re-check next tick.
946
1630
  if ((_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.isMaintenanceActive())
947
1631
  return;
948
- if (this.projectLocks.size > 0)
1632
+ if (this.projectMutex.pending > 0 || this.operations.activeCount > 0)
949
1633
  return;
950
1634
  const reason = ageExceeded
951
1635
  ? `age ${(ageMs / 3600000).toFixed(1)}h > ${(MAX_LIFETIME_MS / 3600000).toFixed(1)}h`
@@ -954,22 +1638,43 @@ class Daemon {
954
1638
  this.recycling = true;
955
1639
  void this.shutdown({ relaunch: true }).finally(() => process.exit(0));
956
1640
  }
957
- shutdown() {
958
- return __awaiter(this, arguments, void 0, function* (opts = {}) {
1641
+ shutdown(opts = {}) {
1642
+ if (!this.shutdownPromise) {
1643
+ this.shutdownPromise = this.performShutdown(opts);
1644
+ }
1645
+ return this.shutdownPromise;
1646
+ }
1647
+ performShutdown(opts) {
1648
+ return __awaiter(this, void 0, void 0, function* () {
959
1649
  var _a, _b, _c, _d;
960
- if (this.shuttingDown)
961
- return;
962
1650
  this.shuttingDown = true;
1651
+ this.ready = false;
1652
+ const strictResourceShutdown = this.hasUnfinishedRebuild();
963
1653
  console.log("[daemon] Shutting down...");
964
1654
  // Announce graceful shutdown BEFORE dropping the liveness markers below, so a
965
1655
  // successor spawned during the (possibly long) drain sees the draining marker
966
1656
  // and defers instead of SIGKILLing us mid-cleanup.
967
1657
  (0, daemon_client_1.writeDrainingMarker)(process.pid);
968
- // Drop external liveness markers FIRST so the next daemon start isn't
969
- // fooled by leftover state if the long cleanup below is interrupted
970
- // (uncaught exception, second SIGTERM, OOM kill mid-shutdown). The
971
- // fresh-lock check in isDaemonHeartbeatFresh keyed on these — orphans
972
- // here used to cause silent no-op spawns for up to 150s.
1658
+ // Stop accepting new IPC before draining admitted operations. Existing
1659
+ // sockets are destroyed below so their close handlers cancel queued work.
1660
+ const server = this.server;
1661
+ this.server = null;
1662
+ const serverClosed = new Promise((resolve) => {
1663
+ if (!(server === null || server === void 0 ? void 0 : server.listening)) {
1664
+ resolve();
1665
+ return;
1666
+ }
1667
+ server.close(() => resolve());
1668
+ });
1669
+ for (const connection of this.connections)
1670
+ connection.end();
1671
+ const forceCloseConnections = setTimeout(() => {
1672
+ for (const connection of this.connections)
1673
+ connection.destroy();
1674
+ }, 1000);
1675
+ forceCloseConnections.unref();
1676
+ // Drop external liveness markers now so interrupted cleanup cannot leave a
1677
+ // fresh-looking daemon that silently blocks its successor.
973
1678
  try {
974
1679
  fs.unlinkSync(config_1.PATHS.daemonSocket);
975
1680
  }
@@ -981,44 +1686,60 @@ class Daemon {
981
1686
  if (this.releaseLock) {
982
1687
  const release = this.releaseLock;
983
1688
  this.releaseLock = null;
984
- release().catch(() => { });
1689
+ try {
1690
+ yield release();
1691
+ }
1692
+ catch (_g) { }
985
1693
  }
986
1694
  if (this.heartbeatInterval)
987
1695
  clearInterval(this.heartbeatInterval);
988
1696
  if (this.idleInterval)
989
1697
  clearInterval(this.idleInterval);
990
- // Abort in-flight index/add operations so they exit promptly
991
- for (const ac of this.shutdownAbortControllers) {
992
- ac.abort();
993
- }
994
- // Wait for in-flight project operations to finish (they check shuttingDown/signal)
995
- const pendingLocks = [...this.projectLocks.values()];
996
- if (pendingLocks.length > 0) {
997
- console.log(`[daemon] Waiting for ${pendingLocks.length} in-flight operation(s)...`);
998
- yield Promise.allSettled(pendingLocks);
1698
+ for (const timer of this.pendingIndexRetryTimers.values()) {
1699
+ clearTimeout(timer);
999
1700
  }
1000
- // Close all processors
1001
- for (const processor of this.processors.values()) {
1002
- yield processor.close();
1701
+ this.pendingIndexRetryTimers.clear();
1702
+ this.pendingIndexRetryCounts.clear();
1703
+ // Abort explicit operation controllers, close coordinator admission, and
1704
+ // reject queued project waiters. Start these drains before watcher teardown
1705
+ // so no timer or socket can admit replacement work.
1706
+ for (const ac of this.shutdownAbortControllers) {
1707
+ ac.abort(new operation_coordinator_1.OperationClosedError());
1003
1708
  }
1709
+ const operationDrain = this.operations.close();
1710
+ const projectDrain = this.projectMutex.close();
1711
+ // Abort catchup/recovery and remove each processor before worker teardown.
1712
+ yield this.watcherManager.quiesceAll();
1713
+ yield Promise.all([operationDrain, projectDrain]);
1004
1714
  // Stop LLM server if running
1005
1715
  try {
1006
1716
  yield ((_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.stop());
1007
1717
  }
1008
- catch (_g) { }
1009
- // Stop MLX embed server if we started it
1010
- this.mlxServerManager.stopMlxServer();
1718
+ catch (_h) { }
1011
1719
  // Destroy worker pool to prevent orphaned child processes
1720
+ if (this.workerPool) {
1721
+ if (strictResourceShutdown) {
1722
+ yield this.workerPool.destroy({
1723
+ requireExit: true,
1724
+ });
1725
+ }
1726
+ else {
1727
+ try {
1728
+ yield this.workerPool.destroy();
1729
+ }
1730
+ catch (_j) { }
1731
+ }
1732
+ this.workerPool = null;
1733
+ this.resources = null;
1734
+ }
1012
1735
  if ((0, pool_1.isWorkerPoolInitialized)()) {
1013
1736
  try {
1014
1737
  yield (0, pool_1.destroyWorkerPool)();
1015
1738
  }
1016
- catch (_h) { }
1739
+ catch (_k) { }
1017
1740
  }
1018
- // Stop watcher poll intervals + FSEvents recovery probes, unsubscribe all
1019
- yield this.watcherManager.teardown();
1020
- // Close server (socket/pid/lock already dropped at the top of shutdown)
1021
- (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
1741
+ // Stop MLX embed server only after worker requests have drained.
1742
+ yield this.mlxServerManager.stopMlxServer();
1022
1743
  // Unregister all
1023
1744
  for (const root of this.processors.keys()) {
1024
1745
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -1027,19 +1748,27 @@ class Daemon {
1027
1748
  this.processors.clear();
1028
1749
  // Close shared resources
1029
1750
  try {
1030
- yield ((_c = this.metaCache) === null || _c === void 0 ? void 0 : _c.close());
1751
+ yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
1031
1752
  }
1032
- catch (_j) { }
1033
- try {
1034
- yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
1753
+ catch (_l) { }
1754
+ if (strictResourceShutdown) {
1755
+ yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close({ requireClosed: true }));
1756
+ }
1757
+ else {
1758
+ try {
1759
+ yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
1760
+ }
1761
+ catch (_m) { }
1035
1762
  }
1036
- catch (_k) { }
1763
+ yield serverClosed;
1764
+ clearTimeout(forceCloseConnections);
1765
+ this.connections.clear();
1037
1766
  // Hand off to a successor only after every resource is released and the
1038
1767
  // liveness markers (socket/pid/lock) are already gone — so the fresh
1039
1768
  // daemon's singleton check sees a clean slate and opens LanceDB/LMDB
1040
1769
  // without contending with this exiting process.
1041
1770
  if (opts.relaunch) {
1042
- const pid = (0, daemon_launcher_1.spawnDaemon)();
1771
+ const pid = yield (0, daemon_launcher_1.spawnDaemon)();
1043
1772
  console.log(`[daemon] Spawned successor daemon${pid ? ` (PID: ${pid})` : " (spawn failed)"}`);
1044
1773
  }
1045
1774
  // Cleanly drained — drop the marker so a later start doesn't defer to a