grepmax 0.24.0 → 0.25.0

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 +11 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +16 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +76 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/index.js +23 -24
  9. package/dist/commands/list.js +23 -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 +5 -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 +1101 -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 +23 -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,154 @@ 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
+ return this.watcherManager.watchProject(root);
472
+ }
473
+ runSharedOperation(name, signal, fn) {
474
+ return this.operations.runShared(name, signal, fn);
475
+ }
476
+ operationStatus() {
477
+ return this.operations.status;
478
+ }
479
+ resourceGenerationId() {
480
+ var _a, _b;
481
+ return (_b = (_a = this.resources) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
482
+ }
483
+ hasUnfinishedRebuild() {
484
+ try {
485
+ return (0, project_registry_1.hasUnfinishedProjectRebuild)();
486
+ }
487
+ catch (_a) {
488
+ return true;
489
+ }
490
+ }
491
+ schedulePendingIndexRetry(root) {
492
+ var _a;
493
+ if (this.shuttingDown || this.pendingIndexRetryTimers.has(root))
494
+ return;
495
+ const attempts = (_a = this.pendingIndexRetryCounts.get(root)) !== null && _a !== void 0 ? _a : 0;
496
+ if (attempts >= 5)
497
+ return;
498
+ this.pendingIndexRetryCounts.set(root, attempts + 1);
499
+ const timer = setTimeout(() => {
500
+ this.pendingIndexRetryTimers.delete(root);
501
+ if (!this.shuttingDown)
502
+ void this.indexPendingProject(root);
503
+ }, 30000);
504
+ timer.unref();
505
+ this.pendingIndexRetryTimers.set(root, timer);
506
+ }
507
+ clearPendingIndexRetry(root) {
508
+ const timer = this.pendingIndexRetryTimers.get(root);
509
+ if (timer)
510
+ clearTimeout(timer);
511
+ this.pendingIndexRetryTimers.delete(root);
512
+ this.pendingIndexRetryCounts.delete(root);
397
513
  }
398
514
  indexPendingProject(root) {
399
515
  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
- }));
516
+ const ac = new AbortController();
517
+ this.shutdownAbortControllers.add(ac);
518
+ try {
519
+ yield this.withProjectLock(root, ac.signal, () => __awaiter(this, void 0, void 0, function* () {
520
+ return this.runSharedOperation("index-pending", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
521
+ var _a, _b, _c, _d;
522
+ // Bail if shutdown raced ahead of us between iteration and lock
523
+ // acquisition. Starting now would race the store close below.
524
+ if (this.shuttingDown)
525
+ return;
526
+ if (!this.vectorDb || !this.metaCache)
527
+ return;
528
+ const current = (0, project_registry_1.getProject)(root);
529
+ if ((current === null || current === void 0 ? void 0 : current.status) !== "pending" && (current === null || current === void 0 ? void 0 : current.status) !== "error")
530
+ return;
531
+ const name = path.basename(root);
532
+ const start = Date.now();
533
+ (0, logger_1.log)("daemon", `indexPendingProject start: ${name} (${root})`);
534
+ this.vectorDb.pauseMaintenanceLoop();
535
+ try {
536
+ if (this.processors.has(root))
537
+ yield this.unwatchProjectWithinOperation(root);
538
+ const result = yield (0, syncer_1.initialSync)({
539
+ projectRoot: root,
540
+ vectorDb: this.vectorDb,
541
+ metaCache: this.metaCache,
542
+ signal: operationSignal,
543
+ generation: (_a = this.activeGeneration) !== null && _a !== void 0 ? _a : undefined,
544
+ embedMode: (_b = this.activeConfig) === null || _b === void 0 ? void 0 : _b.embedMode,
545
+ workerPool: (_c = this.workerPool) !== null && _c !== void 0 ? _c : undefined,
546
+ onProgress: () => {
547
+ this.resetActivity();
548
+ },
549
+ });
550
+ const prefix = root.endsWith("/") ? root : `${root}/`;
551
+ const chunkCount = yield this.vectorDb.countRowsForPath(prefix);
552
+ const proj = (0, project_registry_1.getProject)(root);
553
+ if (proj) {
554
+ if (result.degraded) {
555
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { status: "pending" }));
556
+ this.schedulePendingIndexRetry(root);
557
+ }
558
+ else {
559
+ this.clearPendingIndexRetry(root);
560
+ (0, project_registry_1.stampProjectFullSync)({
561
+ root,
562
+ name: proj.name,
563
+ generation: result.generation,
564
+ embedMode: result.embedMode,
565
+ chunkCount,
566
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
567
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
568
+ expectedRebuildId: result.registryExpectation.rebuildId,
569
+ });
570
+ }
571
+ }
572
+ (0, logger_1.log)("daemon", `indexPendingProject done: ${name} — ${result.total} files, ${result.indexed} chunks, ${Date.now() - start}ms`);
573
+ }
574
+ catch (err) {
575
+ const msg = err instanceof Error ? err.message : String(err);
576
+ console.error(`[daemon] indexPendingProject failed for ${name} after ${Date.now() - start}ms: ${msg}`);
577
+ const proj = (0, project_registry_1.getProject)(root);
578
+ if (proj && !(err instanceof project_registry_1.ProjectRegistryConflictError)) {
579
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { status: "error" }));
580
+ this.schedulePendingIndexRetry(root);
581
+ }
582
+ }
583
+ finally {
584
+ (_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.resumeMaintenanceLoop();
585
+ if (!this.shuttingDown) {
586
+ try {
587
+ yield this.watchProjectWithinOperation(root);
588
+ }
589
+ catch (err) {
590
+ console.error(`[daemon] Failed to re-watch ${name}:`, err);
591
+ }
592
+ }
593
+ }
594
+ }));
595
+ }));
596
+ }
597
+ finally {
598
+ this.shutdownAbortControllers.delete(ac);
599
+ }
441
600
  });
442
601
  }
443
- unwatchProject(root) {
444
- return __awaiter(this, void 0, void 0, function* () {
445
- return this.watcherManager.unwatchProject(root);
446
- });
602
+ unwatchProject(root, signal) {
603
+ return this.withProjectLock(root, signal, () => this.runSharedOperation("unwatch", signal, () => this.unwatchProjectWithinOperation(root)));
604
+ }
605
+ unwatchProjectWithinOperation(root) {
606
+ return this.watcherManager.unwatchProject(root);
447
607
  }
448
608
  /**
449
609
  * Run a search inside the daemon, reusing the warm VectorDB connection,
@@ -485,16 +645,20 @@ class Daemon {
485
645
  // Search handling lives in search-handler.ts (Phase 12 split). The daemon
486
646
  // supplies its warm VectorDB + watcher/index bookkeeping; the handler runs
487
647
  // 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);
648
+ return this.runSharedOperation("search", signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
649
+ return (0, search_handler_1.handleDaemonSearch)({
650
+ vectorDb: this.vectorDb,
651
+ processors: this.processors,
652
+ indexProgress: this.indexProgress,
653
+ searchers: this.searchers,
654
+ getIndexState: (root) => this.indexState(root),
655
+ touchActivity: () => {
656
+ this.lastActivity = Date.now();
657
+ },
658
+ generation: this.activeGeneration,
659
+ workerPool: this.workerPool,
660
+ }, payload, operationSignal);
661
+ }));
498
662
  });
499
663
  }
500
664
  listProjects() {
@@ -514,90 +678,250 @@ class Daemon {
514
678
  var _a, _b;
515
679
  return (_b = (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.diskPressure) !== null && _b !== void 0 ? _b : "unknown";
516
680
  }
681
+ getMlxStatus() {
682
+ return this.mlxServerManager.getStatus();
683
+ }
517
684
  /** Reset idle timer — call during long-running operations. */
518
685
  resetActivity() {
519
686
  this.lastActivity = Date.now();
520
687
  }
521
688
  // --- Per-project operation serialization ---
522
- withProjectLock(root, fn) {
689
+ withProjectLock(root, signal, fn) {
523
690
  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;
691
+ return this.projectMutex.run(root, signal, fn);
692
+ });
693
+ }
694
+ // --- Streaming write operations (IPC) ---
695
+ activeConfigurationError() {
696
+ const activeConfig = this.activeConfig;
697
+ const activeGeneration = this.activeGeneration;
698
+ if (!activeConfig || !activeGeneration)
699
+ return "daemon resources not ready";
700
+ const currentConfig = (0, index_config_1.readGlobalConfig)();
701
+ let currentGeneration;
702
+ try {
703
+ currentGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(currentConfig);
704
+ }
705
+ catch (error) {
706
+ return error instanceof Error ? error.message : String(error);
707
+ }
708
+ return currentGeneration.fingerprint !== activeGeneration.fingerprint ||
709
+ currentConfig.embedMode !== activeConfig.embedMode ||
710
+ currentConfig.mlxModel !== activeConfig.mlxModel
711
+ ? "daemon configuration is stale; restart the gmax daemon"
712
+ : null;
713
+ }
714
+ ensureProject(root, conn) {
715
+ return __awaiter(this, void 0, void 0, function* () {
716
+ const ac = new AbortController();
717
+ const onClose = () => ac.abort();
718
+ conn.once("close", onClose);
719
+ this.shutdownAbortControllers.add(ac);
532
720
  try {
533
- return yield fn();
721
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("ensure-project", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
722
+ var _a, _b;
723
+ if (operationSignal.aborted || this.shuttingDown)
724
+ return;
725
+ const activeConfig = this.activeConfig;
726
+ const configurationError = this.activeConfigurationError();
727
+ if (!activeConfig || configurationError) {
728
+ (0, ipc_handler_1.writeDone)(conn, {
729
+ ok: false,
730
+ error: configurationError !== null && configurationError !== void 0 ? configurationError : "daemon resources not ready",
731
+ });
732
+ return;
733
+ }
734
+ const project = (0, project_registry_1.getProject)(root);
735
+ if ((project === null || project === void 0 ? void 0 : project.status) === "indexed") {
736
+ if (!this.activeGeneration ||
737
+ (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration)
738
+ .state === "stale") {
739
+ (0, ipc_handler_1.writeDone)(conn, {
740
+ ok: false,
741
+ error: "project embedding generation is stale; rebuild the project",
742
+ });
743
+ return;
744
+ }
745
+ yield this.watchProjectWithinOperation(root);
746
+ (0, ipc_handler_1.writeDone)(conn, { ok: true, status: "indexed", watched: true });
747
+ return;
748
+ }
749
+ (0, project_registry_1.registerProject)({
750
+ root,
751
+ name: (_a = project === null || project === void 0 ? void 0 : project.name) !== null && _a !== void 0 ? _a : path.basename(root),
752
+ vectorDim: activeConfig.vectorDim,
753
+ modelTier: activeConfig.modelTier,
754
+ embedMode: activeConfig.embedMode,
755
+ lastIndexed: (_b = project === null || project === void 0 ? void 0 : project.lastIndexed) !== null && _b !== void 0 ? _b : "",
756
+ chunkCount: project === null || project === void 0 ? void 0 : project.chunkCount,
757
+ status: "pending",
758
+ chunkerVersion: project === null || project === void 0 ? void 0 : project.chunkerVersion,
759
+ });
760
+ yield this.addProjectLocked(root, conn, operationSignal, project);
761
+ })));
534
762
  }
535
763
  finally {
536
- release();
537
- if (this.projectLocks.get(root) === next) {
538
- this.projectLocks.delete(root);
539
- }
764
+ conn.off("close", onClose);
765
+ this.shutdownAbortControllers.delete(ac);
540
766
  }
541
767
  });
542
768
  }
543
- // --- Streaming write operations (IPC) ---
544
769
  addProject(root, conn) {
545
770
  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);
771
+ const ac = new AbortController();
772
+ const onClose = () => ac.abort();
773
+ conn.once("close", onClose);
774
+ this.shutdownAbortControllers.add(ac);
775
+ try {
776
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("add-project", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
777
+ if (operationSignal.aborted || this.shuttingDown)
778
+ return;
779
+ const configurationError = this.activeConfigurationError();
780
+ if (configurationError) {
781
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: configurationError });
782
+ return;
580
783
  }
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,
784
+ yield this.addProjectLocked(root, conn, operationSignal, (0, project_registry_1.getProject)(root));
785
+ })));
786
+ }
787
+ finally {
788
+ conn.off("close", onClose);
789
+ this.shutdownAbortControllers.delete(ac);
790
+ }
791
+ });
792
+ }
793
+ addProjectLocked(root, conn, signal, previousProject) {
794
+ return __awaiter(this, void 0, void 0, function* () {
795
+ var _a, _b, _c, _d;
796
+ if (!this.vectorDb || !this.metaCache || !this.activeConfig) {
797
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
798
+ return;
799
+ }
800
+ if (!(0, project_registry_1.getProject)(root)) {
801
+ (0, project_registry_1.registerProject)({
802
+ root,
803
+ name: path.basename(root),
804
+ vectorDim: this.activeConfig.vectorDim,
805
+ modelTier: this.activeConfig.modelTier,
806
+ embedMode: this.activeConfig.embedMode,
807
+ lastIndexed: "",
808
+ status: "pending",
809
+ });
810
+ }
811
+ this.vectorDb.pauseMaintenanceLoop();
812
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
813
+ let lastProgressTime = 0;
814
+ try {
815
+ const result = yield (0, syncer_1.initialSync)({
816
+ projectRoot: root,
817
+ vectorDb: this.vectorDb,
818
+ metaCache: this.metaCache,
819
+ signal,
820
+ generation: (_a = this.activeGeneration) !== null && _a !== void 0 ? _a : undefined,
821
+ embedMode: this.activeConfig.embedMode,
822
+ workerPool: (_b = this.workerPool) !== null && _b !== void 0 ? _b : undefined,
823
+ onProgress: (info) => {
824
+ this.resetActivity();
825
+ const now = Date.now();
826
+ if (now - lastProgressTime < 100)
827
+ return;
828
+ lastProgressTime = now;
829
+ (0, ipc_handler_1.writeProgress)(conn, {
830
+ processed: info.processed,
831
+ indexed: info.indexed,
832
+ total: info.total,
833
+ filePath: info.filePath,
834
+ });
835
+ },
836
+ });
837
+ if (signal.aborted || this.shuttingDown) {
838
+ const abortError = new Error("Aborted");
839
+ abortError.name = "AbortError";
840
+ throw abortError;
841
+ }
842
+ const prefix = root.endsWith("/") ? root : `${root}/`;
843
+ const chunkCount = yield this.vectorDb.countRowsForPath(prefix);
844
+ const project = (0, project_registry_1.getProject)(root);
845
+ if (result.degraded) {
846
+ if (previousProject)
847
+ (0, project_registry_1.registerProject)(previousProject);
848
+ this.schedulePendingIndexRetry(root);
849
+ }
850
+ else {
851
+ this.clearPendingIndexRetry(root);
852
+ (0, project_registry_1.stampProjectFullSync)({
853
+ root,
854
+ name: (_c = project === null || project === void 0 ? void 0 : project.name) !== null && _c !== void 0 ? _c : path.basename(root),
855
+ generation: result.generation,
856
+ embedMode: result.embedMode,
857
+ chunkCount,
858
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
859
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
860
+ expectedRebuildId: result.registryExpectation.rebuildId,
588
861
  });
589
862
  }
590
- catch (err) {
863
+ yield this.watchProjectWithinOperation(root);
864
+ (0, ipc_handler_1.writeDone)(conn, {
865
+ ok: true,
866
+ processed: result.processed,
867
+ indexed: result.indexed,
868
+ total: result.total,
869
+ failedFiles: result.failedFiles,
870
+ degraded: result.degraded,
871
+ scanErrors: result.scanErrors,
872
+ });
873
+ }
874
+ catch (err) {
875
+ const aborted = signal.aborted || (err === null || err === void 0 ? void 0 : err.name) === "AbortError";
876
+ if (aborted) {
877
+ if (previousProject)
878
+ (0, project_registry_1.registerProject)(previousProject);
879
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "aborted" });
880
+ }
881
+ else {
591
882
  const msg = err instanceof Error ? err.message : String(err);
592
883
  console.error(`[daemon] addProject failed for ${path.basename(root)}:`, msg);
593
- stopHeartbeat();
884
+ const project = (0, project_registry_1.getProject)(root);
885
+ if (project && !(err instanceof project_registry_1.ProjectRegistryConflictError)) {
886
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, project), { status: "error" }));
887
+ }
594
888
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
595
889
  }
596
- finally {
597
- stopHeartbeat();
598
- this.shutdownAbortControllers.delete(ac);
599
- (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
600
- }
890
+ }
891
+ finally {
892
+ stopHeartbeat();
893
+ (_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.resumeMaintenanceLoop();
894
+ }
895
+ });
896
+ }
897
+ projectStats(root) {
898
+ return __awaiter(this, void 0, void 0, function* () {
899
+ return this.runSharedOperation("project-stats", undefined, () => __awaiter(this, void 0, void 0, function* () {
900
+ if (!this.vectorDb)
901
+ throw new Error("daemon resources not ready");
902
+ const project = (0, project_registry_1.getProject)(root);
903
+ if (!project)
904
+ throw new Error("project not registered");
905
+ if (!this.activeConfig)
906
+ throw new Error("daemon resources not ready");
907
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, this.activeConfig);
908
+ const prefix = root.endsWith("/") ? root : `${root}/`;
909
+ const [chunks, files] = yield Promise.all([
910
+ this.vectorDb.countRowsForPath(prefix),
911
+ this.vectorDb.countDistinctFilesForPath(prefix),
912
+ ]);
913
+ return {
914
+ files,
915
+ chunks,
916
+ vectorDim: project.vectorDim,
917
+ modelTier: project.modelTier,
918
+ embedMode: project.embedMode,
919
+ indexedAt: project.lastIndexed,
920
+ watching: this.processors.has(root),
921
+ configuredEmbedding: identity.configured,
922
+ builtEmbedding: identity.built,
923
+ embeddingState: identity.state,
924
+ };
601
925
  }));
602
926
  });
603
927
  }
@@ -612,20 +936,13 @@ class Daemon {
612
936
  */
613
937
  reindexOneProject(root, opts, signal, onProgress) {
614
938
  return __awaiter(this, void 0, void 0, function* () {
939
+ var _a, _b, _c;
615
940
  if (!this.vectorDb || !this.metaCache) {
616
941
  throw new Error("daemon resources not ready");
617
942
  }
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
- }
943
+ // Quiesce the subscription, processor, and any catchup generation before
944
+ // full sync takes deletion authority for this project.
945
+ yield this.unwatchProjectWithinOperation(root);
629
946
  // Mark this root as full-indexing so concurrent searches get a
630
947
  // partial-result footer (Phase 6); seeded at 0/0 until the first tick.
631
948
  this.indexProgress.set(root, { processed: 0, total: 0 });
@@ -637,6 +954,9 @@ class Daemon {
637
954
  vectorDb: this.vectorDb,
638
955
  metaCache: this.metaCache,
639
956
  signal,
957
+ generation: (_a = this.activeGeneration) !== null && _a !== void 0 ? _a : undefined,
958
+ embedMode: (_b = this.activeConfig) === null || _b === void 0 ? void 0 : _b.embedMode,
959
+ workerPool: (_c = this.workerPool) !== null && _c !== void 0 ? _c : undefined,
640
960
  onProgress: (info) => {
641
961
  this.resetActivity();
642
962
  this.indexProgress.set(root, {
@@ -650,9 +970,9 @@ class Daemon {
650
970
  finally {
651
971
  this.indexProgress.delete(root);
652
972
  // Re-enable watcher (skip if shutting down)
653
- if (!this.shuttingDown) {
973
+ if (!this.shuttingDown && opts.rewatch !== false) {
654
974
  try {
655
- yield this.watchProject(root);
975
+ yield this.watchProjectWithinOperation(root);
656
976
  }
657
977
  catch (err) {
658
978
  console.error(`[daemon] Failed to re-watch ${path.basename(root)}:`, err);
@@ -663,52 +983,78 @@ class Daemon {
663
983
  }
664
984
  indexProject(root, conn, opts) {
665
985
  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,
986
+ const ac = new AbortController();
987
+ const onClose = () => ac.abort();
988
+ conn.once("close", onClose);
989
+ this.shutdownAbortControllers.add(ac);
990
+ try {
991
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("index-project", ac.signal, (operationSignal) => __awaiter(this, void 0, void 0, function* () {
992
+ var _a, _b;
993
+ if (!this.vectorDb || !this.metaCache) {
994
+ (0, ipc_handler_1.writeDone)(conn, {
995
+ ok: false,
996
+ error: "daemon resources not ready",
689
997
  });
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
- }));
998
+ return;
999
+ }
1000
+ this.vectorDb.pauseMaintenanceLoop();
1001
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
1002
+ let lastProgressTime = 0;
1003
+ try {
1004
+ const result = yield this.reindexOneProject(root, opts, operationSignal, (info) => {
1005
+ const now = Date.now();
1006
+ if (now - lastProgressTime < 100)
1007
+ return;
1008
+ lastProgressTime = now;
1009
+ (0, ipc_handler_1.writeProgress)(conn, {
1010
+ processed: info.processed,
1011
+ indexed: info.indexed,
1012
+ total: info.total,
1013
+ filePath: info.filePath,
1014
+ });
1015
+ });
1016
+ if (!opts.dryRun && !result.degraded) {
1017
+ const prefix = root.endsWith("/") ? root : `${root}/`;
1018
+ const chunkCount = yield this.vectorDb.countRowsForPath(prefix);
1019
+ const project = (0, project_registry_1.getProject)(root);
1020
+ (0, project_registry_1.stampProjectFullSync)({
1021
+ root,
1022
+ generation: result.generation,
1023
+ embedMode: result.embedMode,
1024
+ chunkCount,
1025
+ chunkerVersion: opts.reset
1026
+ ? config_1.CONFIG.CHUNKER_VERSION
1027
+ : ((_a = project === null || project === void 0 ? void 0 : project.chunkerVersion) !== null && _a !== void 0 ? _a : 1),
1028
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
1029
+ expectedRebuildId: result.registryExpectation.rebuildId,
1030
+ });
1031
+ }
1032
+ (0, ipc_handler_1.writeDone)(conn, {
1033
+ ok: true,
1034
+ processed: result.processed,
1035
+ indexed: result.indexed,
1036
+ total: result.total,
1037
+ failedFiles: result.failedFiles,
1038
+ degraded: result.degraded,
1039
+ scanErrors: result.scanErrors,
1040
+ embeddingFingerprint: result.generation.fingerprint,
1041
+ });
1042
+ }
1043
+ catch (err) {
1044
+ const msg = err instanceof Error ? err.message : String(err);
1045
+ console.error(`[daemon] indexProject failed for ${path.basename(root)}:`, msg);
1046
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
1047
+ }
1048
+ finally {
1049
+ stopHeartbeat();
1050
+ (_b = this.vectorDb) === null || _b === void 0 ? void 0 : _b.resumeMaintenanceLoop();
1051
+ }
1052
+ })));
1053
+ }
1054
+ finally {
1055
+ conn.off("close", onClose);
1056
+ this.shutdownAbortControllers.delete(ac);
1057
+ }
712
1058
  });
713
1059
  }
714
1060
  /**
@@ -724,179 +1070,508 @@ class Daemon {
724
1070
  repairRebuild(conn) {
725
1071
  return __awaiter(this, void 0, void 0, function* () {
726
1072
  var _a;
727
- if (!this.vectorDb || !this.metaCache) {
1073
+ const oldResources = this.resources;
1074
+ const metaCache = this.metaCache;
1075
+ if (!oldResources || !metaCache) {
728
1076
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
729
1077
  return;
730
1078
  }
731
- const ac = new AbortController();
732
- conn.on("close", () => ac.abort());
733
- this.shutdownAbortControllers.add(ac);
734
- this.vectorDb.pauseMaintenanceLoop();
1079
+ const targetConfig = (0, index_config_1.readGlobalConfig)();
1080
+ const targetGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(targetConfig);
1081
+ const clientAbort = new AbortController();
1082
+ let dropCommitted = false;
1083
+ let desiredWatchRoots = [];
1084
+ let watchersRestored = false;
1085
+ let oldPoolDestroyed = false;
1086
+ let oldDbClosed = false;
1087
+ const failClosedOldResources = (lease) => __awaiter(this, void 0, void 0, function* () {
1088
+ this.ready = false;
1089
+ this.resources = null;
1090
+ this.vectorDb = null;
1091
+ this.workerPool = null;
1092
+ if (!oldPoolDestroyed) {
1093
+ yield oldResources.workerPool
1094
+ .destroy({ requireExit: true })
1095
+ .catch(() => { });
1096
+ oldPoolDestroyed = true;
1097
+ }
1098
+ if (!oldDbClosed) {
1099
+ yield oldResources.vectorDb.close().catch(() => { });
1100
+ oldDbClosed = true;
1101
+ }
1102
+ yield lease.release().catch(() => { });
1103
+ });
1104
+ const onClose = () => {
1105
+ if (!dropCommitted)
1106
+ clientAbort.abort(new Error("client disconnected"));
1107
+ };
1108
+ conn.once("close", onClose);
735
1109
  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;
1110
+ const throwIfPreDropCancelled = (operationSignal) => {
1111
+ if (operationSignal.aborted)
1112
+ throw operationSignal.reason;
1113
+ if (!dropCommitted && clientAbort.signal.aborted) {
1114
+ throw clientAbort.signal.reason;
1115
+ }
1116
+ };
742
1117
  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;
1118
+ const result = yield this.operations.runExclusive("repair", () => __awaiter(this, void 0, void 0, function* () {
1119
+ desiredWatchRoots = yield this.watcherManager.quiesceAll();
1120
+ }), (operationSignal) => __awaiter(this, void 0, void 0, function* () {
1121
+ throwIfPreDropCancelled(operationSignal);
1122
+ oldResources.vectorDb.pauseMaintenanceLoop();
1123
+ this.searchers.clear();
1124
+ (0, ipc_handler_1.writeProgress)(conn, {
1125
+ phase: "lease",
1126
+ message: "waiting for exclusive store ownership",
1127
+ });
1128
+ let exclusiveLease = null;
1129
+ try {
1130
+ exclusiveLease = yield oldResources.vectorDb.upgradeStoreLease(AbortSignal.any([operationSignal, clientAbort.signal]));
1131
+ throwIfPreDropCancelled(operationSignal);
1132
+ }
1133
+ catch (error) {
1134
+ if (exclusiveLease) {
1135
+ try {
1136
+ yield oldResources.vectorDb.downgradeStoreLease();
1137
+ }
1138
+ catch (downgradeError) {
1139
+ yield failClosedOldResources(exclusiveLease);
1140
+ throw new Error(`Failed to restore shared store ownership: ${String(downgradeError)}`);
1141
+ }
1142
+ }
1143
+ oldResources.vectorDb.resumeMaintenanceLoop();
1144
+ throw error;
1145
+ }
1146
+ if (!exclusiveLease) {
1147
+ throw new Error("Exclusive store lease acquisition returned no lease");
1148
+ }
1149
+ let reservation;
1150
+ try {
1151
+ reservation = (0, project_registry_1.reserveProjectsForRebuild)(targetGeneration);
1152
+ }
1153
+ catch (error) {
1154
+ try {
1155
+ yield oldResources.vectorDb.downgradeStoreLease();
1156
+ }
1157
+ catch (downgradeError) {
1158
+ yield failClosedOldResources(exclusiveLease);
1159
+ throw new Error(`Failed to restore shared store ownership: ${String(downgradeError)}`);
1160
+ }
1161
+ oldResources.vectorDb.resumeMaintenanceLoop();
1162
+ throw error;
1163
+ }
1164
+ let targetDb = null;
1165
+ let targetPool = null;
1166
+ let published = false;
1167
+ try {
1168
+ (0, ipc_handler_1.writeProgress)(conn, {
1169
+ phase: "prepare",
1170
+ rebuildId: reservation.rebuildId,
1171
+ projects: reservation.reserved.length,
1172
+ });
1173
+ yield oldResources.workerPool.destroy({ requireExit: true });
1174
+ oldPoolDestroyed = true;
1175
+ if (oldResources.mlx === "owned") {
1176
+ yield this.mlxServerManager.stopMlxServer();
1177
+ }
1178
+ throwIfPreDropCancelled(operationSignal);
1179
+ yield oldResources.vectorDb.close({
1180
+ releaseLease: false,
1181
+ requireClosed: true,
1182
+ });
1183
+ oldDbClosed = true;
1184
+ throwIfPreDropCancelled(operationSignal);
1185
+ targetDb = this.createVectorDb(targetGeneration.vectorDim, exclusiveLease);
1186
+ (0, project_registry_1.markProjectRebuildDropping)(reservation);
1187
+ try {
1188
+ yield targetDb.drop();
1189
+ dropCommitted = true;
1190
+ conn.off("close", onClose);
1191
+ }
1192
+ catch (error) {
1193
+ // LanceDB does not expose a commit token. Inspect physical state:
1194
+ // an absent table means the destructive commit happened; an
1195
+ // unreadable state is treated as uncertain and therefore post-drop.
1196
+ try {
1197
+ dropCommitted = (yield targetDb.getSchemaVectorDim()) === null;
1198
+ }
1199
+ catch (_a) {
1200
+ dropCommitted = true;
1201
+ }
1202
+ if (dropCommitted)
1203
+ conn.off("close", onClose);
1204
+ throw error;
1205
+ }
1206
+ (0, ipc_handler_1.writeProgress)(conn, {
1207
+ phase: "schema",
1208
+ message: "creating target table",
1209
+ });
1210
+ yield targetDb.ensureTable();
1211
+ const physicalDim = yield targetDb.getSchemaVectorDim();
1212
+ if (physicalDim !== targetGeneration.vectorDim) {
1213
+ throw new Error(`rebuilt table dimension ${physicalDim !== null && physicalDim !== void 0 ? physicalDim : "missing"} does not match target ${targetGeneration.vectorDim}`);
1214
+ }
1215
+ if (targetConfig.embedMode === "gpu") {
1216
+ yield this.mlxServerManager.ensureMlxServer(targetGeneration.mlxModel);
1217
+ }
1218
+ const targetMlx = this.mlxMode(targetConfig);
1219
+ targetPool = this.createWorkerPool(targetGeneration, targetConfig.embedMode);
1220
+ this.publishResourceGeneration(targetConfig, targetGeneration, targetDb, targetPool, targetMlx);
1221
+ published = true;
1222
+ let completed = 0;
1223
+ const failures = [];
1224
+ for (const project of reservation.reserved) {
1225
+ if (operationSignal.aborted)
1226
+ throw operationSignal.reason;
757
1227
  (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,
1228
+ phase: "index",
1229
+ root: project.root,
1230
+ project: project.name,
1231
+ completed,
1232
+ totalProjects: reservation.reserved.length,
766
1233
  });
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 }));
1234
+ try {
1235
+ const sync = yield this.reindexOneProject(project.root, { reset: true, rewatch: false }, operationSignal, (info) => (0, ipc_handler_1.writeProgress)(conn, {
1236
+ phase: "index",
1237
+ root: project.root,
1238
+ project: project.name,
1239
+ processed: info.processed,
1240
+ indexed: info.indexed,
1241
+ total: info.total,
1242
+ filePath: info.filePath,
1243
+ }));
1244
+ if (sync.degraded) {
1245
+ throw new Error(`degraded scan (${sync.failedFiles} failed files)`);
1246
+ }
1247
+ const prefix = project.root.endsWith("/")
1248
+ ? project.root
1249
+ : `${project.root}/`;
1250
+ const chunkCount = yield targetDb.countRowsForPath(prefix);
1251
+ (0, project_registry_1.stampProjectFullSync)({
1252
+ root: project.root,
1253
+ name: project.name,
1254
+ generation: sync.generation,
1255
+ embedMode: sync.embedMode,
1256
+ chunkCount,
1257
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
1258
+ expectedFingerprint: targetGeneration.fingerprint,
1259
+ expectedRebuildId: reservation.rebuildId,
1260
+ });
1261
+ completed++;
1262
+ }
1263
+ catch (error) {
1264
+ failures.push({
1265
+ root: project.root,
1266
+ error: error instanceof Error ? error.message : String(error),
1267
+ });
1268
+ }
772
1269
  }
773
- }));
774
- rebuilt++;
1270
+ yield targetDb.downgradeStoreLease();
1271
+ targetDb.startMaintenanceLoop((fn) => this.runSharedOperation("store-maintenance", undefined, () => fn()));
1272
+ const currentGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)((0, index_config_1.readGlobalConfig)());
1273
+ const configChanged = currentGeneration.fingerprint !== targetGeneration.fingerprint;
1274
+ if (failures.length === 0) {
1275
+ (0, project_registry_1.completeProjectRebuild)(reservation.rebuildId);
1276
+ }
1277
+ return {
1278
+ completed,
1279
+ total: reservation.reserved.length,
1280
+ failures,
1281
+ configChanged,
1282
+ generation: targetGeneration.fingerprint,
1283
+ };
1284
+ }
1285
+ catch (error) {
1286
+ if (!dropCommitted) {
1287
+ this.ready = false;
1288
+ this.resources = null;
1289
+ let recoveryError;
1290
+ yield (targetPool === null || targetPool === void 0 ? void 0 : targetPool.destroy({ requireExit: true }).catch((cause) => {
1291
+ recoveryError !== null && recoveryError !== void 0 ? recoveryError : (recoveryError = cause);
1292
+ }));
1293
+ if (targetDb) {
1294
+ yield targetDb
1295
+ .close({ releaseLease: false, requireClosed: true })
1296
+ .catch((cause) => {
1297
+ recoveryError !== null && recoveryError !== void 0 ? recoveryError : (recoveryError = cause);
1298
+ });
1299
+ }
1300
+ try {
1301
+ (0, project_registry_1.restoreProjectsAfterRebuild)(reservation);
1302
+ }
1303
+ catch (cause) {
1304
+ recoveryError !== null && recoveryError !== void 0 ? recoveryError : (recoveryError = cause);
1305
+ }
1306
+ if (!recoveryError && !oldPoolDestroyed) {
1307
+ try {
1308
+ yield oldResources.vectorDb.downgradeStoreLease();
1309
+ oldResources.vectorDb.resumeMaintenanceLoop();
1310
+ this.resources = oldResources;
1311
+ this.activeConfig = oldResources.config;
1312
+ this.activeGeneration = oldResources.embedding;
1313
+ this.vectorDb = oldResources.vectorDb;
1314
+ this.workerPool = oldResources.workerPool;
1315
+ this.ready = true;
1316
+ }
1317
+ catch (cause) {
1318
+ recoveryError = cause;
1319
+ }
1320
+ }
1321
+ else if (!recoveryError) {
1322
+ let restoredDb = null;
1323
+ let restoredPool = null;
1324
+ try {
1325
+ restoredDb = oldDbClosed
1326
+ ? this.createVectorDb(oldResources.embedding.vectorDim, exclusiveLease)
1327
+ : oldResources.vectorDb;
1328
+ if (oldResources.config.embedMode === "gpu" &&
1329
+ oldResources.mlx === "owned") {
1330
+ yield this.mlxServerManager.ensureMlxServer(oldResources.embedding.mlxModel);
1331
+ }
1332
+ restoredPool = this.createWorkerPool(oldResources.embedding, oldResources.config.embedMode);
1333
+ yield restoredDb.downgradeStoreLease();
1334
+ restoredDb.startMaintenanceLoop((fn) => this.runSharedOperation("store-maintenance", undefined, () => fn()));
1335
+ this.publishResourceGeneration(oldResources.config, oldResources.embedding, restoredDb, restoredPool, this.mlxMode(oldResources.config));
1336
+ this.ready = true;
1337
+ }
1338
+ catch (cause) {
1339
+ recoveryError = cause;
1340
+ yield (restoredPool === null || restoredPool === void 0 ? void 0 : restoredPool.destroy({ requireExit: true }).catch(() => { }));
1341
+ if (restoredDb) {
1342
+ yield restoredDb.close().catch(() => { });
1343
+ }
1344
+ else {
1345
+ yield exclusiveLease.release().catch(() => { });
1346
+ }
1347
+ }
1348
+ }
1349
+ if (recoveryError) {
1350
+ if (!oldPoolDestroyed) {
1351
+ yield oldResources.workerPool
1352
+ .destroy({ requireExit: true })
1353
+ .catch((cause) => {
1354
+ recoveryError = new Error(`Pre-drop rebuild pool cleanup failed: ${String(recoveryError)}; ${String(cause)}`);
1355
+ });
1356
+ oldPoolDestroyed = true;
1357
+ }
1358
+ if (!oldDbClosed) {
1359
+ yield oldResources.vectorDb.close().catch((cause) => {
1360
+ recoveryError = new Error(`Pre-drop rebuild DB cleanup failed: ${String(recoveryError)}; ${String(cause)}`);
1361
+ });
1362
+ oldDbClosed = true;
1363
+ }
1364
+ yield exclusiveLease.release().catch((cause) => {
1365
+ recoveryError = new Error(`Pre-drop rebuild cleanup failed: ${String(recoveryError)}; ${String(cause)}`);
1366
+ });
1367
+ this.resources = null;
1368
+ this.vectorDb = null;
1369
+ this.workerPool = null;
1370
+ this.ready = false;
1371
+ throw new Error(`Pre-drop rebuild recovery failed: ${String(error)}; ${String(recoveryError)}`);
1372
+ }
1373
+ }
1374
+ else {
1375
+ this.ready = false;
1376
+ this.resources = null;
1377
+ this.activeConfig = targetConfig;
1378
+ this.activeGeneration = targetGeneration;
1379
+ this.vectorDb = targetDb;
1380
+ this.workerPool = published ? targetPool : null;
1381
+ if (targetDb) {
1382
+ yield targetDb.downgradeStoreLease().catch(() => { });
1383
+ }
1384
+ else {
1385
+ yield exclusiveLease.release().catch(() => { });
1386
+ }
1387
+ }
1388
+ throw error;
1389
+ }
1390
+ }));
1391
+ for (const root of desiredWatchRoots) {
1392
+ if (((_a = (0, project_registry_1.getProject)(root)) === null || _a === void 0 ? void 0 : _a.status) !== "indexed")
1393
+ continue;
1394
+ try {
1395
+ yield this.watcherManager.watchProject(root, { catchup: false });
1396
+ }
1397
+ catch (error) {
1398
+ result.failures.push({
1399
+ root,
1400
+ error: `watch restore failed: ${error instanceof Error ? error.message : String(error)}`,
1401
+ });
1402
+ }
775
1403
  }
776
- stopHeartbeat();
777
- (0, ipc_handler_1.writeDone)(conn, {
778
- ok: true,
779
- projects: rebuilt,
780
- projectsTotal: projects.length,
781
- indexed: totalIndexed,
782
- });
1404
+ watchersRestored = true;
1405
+ try {
1406
+ yield this.watcherManager.catchupAll(desiredWatchRoots);
1407
+ }
1408
+ catch (error) {
1409
+ result.failures.push({
1410
+ root: "*",
1411
+ error: `watch catchup failed: ${error instanceof Error ? error.message : String(error)}`,
1412
+ });
1413
+ }
1414
+ (0, ipc_handler_1.writeDone)(conn, Object.assign(Object.assign({ ok: result.failures.length === 0 }, result), (result.configChanged
1415
+ ? {
1416
+ warning: "configured embedding changed during rebuild; another rebuild is required",
1417
+ }
1418
+ : {})));
783
1419
  }
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
- });
1420
+ catch (error) {
1421
+ let reportedError = error;
1422
+ if (!dropCommitted && !watchersRestored && this.ready && this.resources) {
1423
+ try {
1424
+ yield this.watcherManager.resumeAll(desiredWatchRoots, {
1425
+ catchup: false,
1426
+ });
1427
+ watchersRestored = true;
1428
+ yield this.watcherManager.catchupAll(desiredWatchRoots);
1429
+ }
1430
+ catch (watchError) {
1431
+ this.ready = false;
1432
+ this.resources = null;
1433
+ reportedError = new Error(`Pre-drop rebuild watcher restoration failed: ${String(error)}; ${String(watchError)}`);
1434
+ }
1435
+ }
1436
+ const details = reportedError instanceof store_lease_1.StoreLeaseTimeoutError
1437
+ ? { blockers: reportedError.blockers }
1438
+ : {};
1439
+ (0, ipc_handler_1.writeDone)(conn, Object.assign({ ok: false, error: reportedError instanceof Error
1440
+ ? reportedError.message
1441
+ : String(reportedError), degraded: dropCommitted }, details));
794
1442
  }
795
1443
  finally {
1444
+ conn.off("close", onClose);
796
1445
  stopHeartbeat();
797
- this.shutdownAbortControllers.delete(ac);
798
- (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
1446
+ if (!this.ready) {
1447
+ setImmediate(() => void this.shutdown());
1448
+ }
799
1449
  }
800
1450
  });
801
1451
  }
802
1452
  removeProject(root, conn) {
803
1453
  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);
1454
+ const ac = new AbortController();
1455
+ const onClose = () => ac.abort();
1456
+ conn.once("close", onClose);
1457
+ this.shutdownAbortControllers.add(ac);
1458
+ try {
1459
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("remove-project", ac.signal, () => __awaiter(this, void 0, void 0, function* () {
1460
+ if (!this.vectorDb || !this.metaCache) {
1461
+ (0, ipc_handler_1.writeDone)(conn, {
1462
+ ok: false,
1463
+ error: "daemon resources not ready",
1464
+ });
1465
+ return;
817
1466
  }
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
- }));
1467
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
1468
+ try {
1469
+ yield this.unwatchProjectWithinOperation(root);
1470
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1471
+ yield this.vectorDb.deletePathsWithPrefix(rootPrefix);
1472
+ const keys = yield this.metaCache.getKeysWithPrefix(rootPrefix);
1473
+ for (const key of keys)
1474
+ this.metaCache.delete(key);
1475
+ (0, ipc_handler_1.writeDone)(conn, { ok: true });
1476
+ }
1477
+ catch (err) {
1478
+ const msg = err instanceof Error ? err.message : String(err);
1479
+ console.error(`[daemon] removeProject failed for ${path.basename(root)}:`, msg);
1480
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
1481
+ }
1482
+ finally {
1483
+ stopHeartbeat();
1484
+ }
1485
+ })));
1486
+ }
1487
+ finally {
1488
+ conn.off("close", onClose);
1489
+ this.shutdownAbortControllers.delete(ac);
1490
+ }
831
1491
  });
832
1492
  }
833
1493
  summarizeProject(root, conn, opts) {
834
1494
  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;
1495
+ const ac = new AbortController();
1496
+ const onClose = () => ac.abort();
1497
+ conn.once("close", onClose);
1498
+ this.shutdownAbortControllers.add(ac);
1499
+ try {
1500
+ yield this.withProjectLock(root, ac.signal, () => this.runSharedOperation("summarize-project", ac.signal, () => __awaiter(this, void 0, void 0, function* () {
1501
+ var _a;
1502
+ if (!this.vectorDb) {
1503
+ (0, ipc_handler_1.writeDone)(conn, {
1504
+ ok: false,
1505
+ error: "daemon resources not ready",
1506
+ });
1507
+ return;
1508
+ }
1509
+ const rootPrefix = (_a = opts.pathPrefix) !== null && _a !== void 0 ? _a : (root.endsWith("/") ? root : `${root}/`);
1510
+ const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
1511
+ let lastProgressTime = 0;
1512
+ try {
1513
+ const result = yield (0, syncer_1.generateSummaries)(this.vectorDb, rootPrefix, (done, total) => {
1514
+ this.resetActivity();
1515
+ const now = Date.now();
1516
+ if (now - lastProgressTime < 100)
1517
+ return;
1518
+ lastProgressTime = now;
1519
+ (0, ipc_handler_1.writeProgress)(conn, { summarized: done, total });
1520
+ }, opts.limit);
1521
+ (0, ipc_handler_1.writeDone)(conn, {
1522
+ ok: true,
1523
+ summarized: result.summarized,
1524
+ remaining: result.remaining,
1525
+ });
1526
+ }
1527
+ catch (err) {
1528
+ const msg = err instanceof Error ? err.message : String(err);
1529
+ console.error(`[daemon] summarizeProject failed for ${path.basename(root)}:`, msg);
1530
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
1531
+ }
1532
+ finally {
1533
+ stopHeartbeat();
1534
+ }
1535
+ })));
1536
+ }
1537
+ finally {
1538
+ conn.off("close", onClose);
1539
+ this.shutdownAbortControllers.delete(ac);
1540
+ }
1541
+ });
1542
+ }
1543
+ // --- LLM server management ---
1544
+ llmStart() {
1545
+ return __awaiter(this, void 0, void 0, function* () {
1546
+ return this.runSharedOperation("llm-start", undefined, () => __awaiter(this, void 0, void 0, function* () {
1547
+ if (!this.llmServer)
1548
+ return { ok: false, error: "daemon not initialized" };
844
1549
  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
- });
1550
+ yield this.llmServer.start();
1551
+ this.resetActivity();
1552
+ return Object.assign({ ok: true }, this.llmServer.getStatus());
859
1553
  }
860
1554
  catch (err) {
861
1555
  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();
1556
+ return { ok: false, error: msg };
868
1557
  }
869
1558
  }));
870
1559
  });
871
1560
  }
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
1561
  llmStop() {
889
1562
  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
- }
1563
+ return this.runSharedOperation("llm-stop", undefined, () => __awaiter(this, void 0, void 0, function* () {
1564
+ if (!this.llmServer)
1565
+ return { ok: false, error: "daemon not initialized" };
1566
+ try {
1567
+ yield this.llmServer.stop();
1568
+ return { ok: true };
1569
+ }
1570
+ catch (err) {
1571
+ const msg = err instanceof Error ? err.message : String(err);
1572
+ return { ok: false, error: msg };
1573
+ }
1574
+ }));
900
1575
  });
901
1576
  }
902
1577
  llmStatus() {
@@ -910,20 +1585,22 @@ class Daemon {
910
1585
  }
911
1586
  reviewCommit(root, commitRef) {
912
1587
  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;
1588
+ return this.runSharedOperation("review", undefined, () => __awaiter(this, void 0, void 0, function* () {
1589
+ this.resetActivity();
1590
+ try {
1591
+ if (!this.llmServer) {
1592
+ console.log("[review] daemon not initialized, skipping");
1593
+ return;
1594
+ }
1595
+ yield this.llmServer.ensure();
1596
+ const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../llm/review")));
1597
+ const result = yield reviewCommit({ commitRef, projectRoot: root });
1598
+ console.log(`[review] ${result.commit} — ${result.findingCount} finding(s) in ${result.duration}s`);
918
1599
  }
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
- }
1600
+ catch (err) {
1601
+ console.error(`[review] failed: ${err instanceof Error ? err.message : String(err)}`);
1602
+ }
1603
+ }));
927
1604
  });
928
1605
  }
929
1606
  /**
@@ -945,7 +1622,7 @@ class Daemon {
945
1622
  // Defer while busy; we'll re-check next tick.
946
1623
  if ((_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.isMaintenanceActive())
947
1624
  return;
948
- if (this.projectLocks.size > 0)
1625
+ if (this.projectMutex.pending > 0 || this.operations.activeCount > 0)
949
1626
  return;
950
1627
  const reason = ageExceeded
951
1628
  ? `age ${(ageMs / 3600000).toFixed(1)}h > ${(MAX_LIFETIME_MS / 3600000).toFixed(1)}h`
@@ -954,22 +1631,43 @@ class Daemon {
954
1631
  this.recycling = true;
955
1632
  void this.shutdown({ relaunch: true }).finally(() => process.exit(0));
956
1633
  }
957
- shutdown() {
958
- return __awaiter(this, arguments, void 0, function* (opts = {}) {
1634
+ shutdown(opts = {}) {
1635
+ if (!this.shutdownPromise) {
1636
+ this.shutdownPromise = this.performShutdown(opts);
1637
+ }
1638
+ return this.shutdownPromise;
1639
+ }
1640
+ performShutdown(opts) {
1641
+ return __awaiter(this, void 0, void 0, function* () {
959
1642
  var _a, _b, _c, _d;
960
- if (this.shuttingDown)
961
- return;
962
1643
  this.shuttingDown = true;
1644
+ this.ready = false;
1645
+ const strictResourceShutdown = this.hasUnfinishedRebuild();
963
1646
  console.log("[daemon] Shutting down...");
964
1647
  // Announce graceful shutdown BEFORE dropping the liveness markers below, so a
965
1648
  // successor spawned during the (possibly long) drain sees the draining marker
966
1649
  // and defers instead of SIGKILLing us mid-cleanup.
967
1650
  (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.
1651
+ // Stop accepting new IPC before draining admitted operations. Existing
1652
+ // sockets are destroyed below so their close handlers cancel queued work.
1653
+ const server = this.server;
1654
+ this.server = null;
1655
+ const serverClosed = new Promise((resolve) => {
1656
+ if (!(server === null || server === void 0 ? void 0 : server.listening)) {
1657
+ resolve();
1658
+ return;
1659
+ }
1660
+ server.close(() => resolve());
1661
+ });
1662
+ for (const connection of this.connections)
1663
+ connection.end();
1664
+ const forceCloseConnections = setTimeout(() => {
1665
+ for (const connection of this.connections)
1666
+ connection.destroy();
1667
+ }, 1000);
1668
+ forceCloseConnections.unref();
1669
+ // Drop external liveness markers now so interrupted cleanup cannot leave a
1670
+ // fresh-looking daemon that silently blocks its successor.
973
1671
  try {
974
1672
  fs.unlinkSync(config_1.PATHS.daemonSocket);
975
1673
  }
@@ -981,44 +1679,60 @@ class Daemon {
981
1679
  if (this.releaseLock) {
982
1680
  const release = this.releaseLock;
983
1681
  this.releaseLock = null;
984
- release().catch(() => { });
1682
+ try {
1683
+ yield release();
1684
+ }
1685
+ catch (_g) { }
985
1686
  }
986
1687
  if (this.heartbeatInterval)
987
1688
  clearInterval(this.heartbeatInterval);
988
1689
  if (this.idleInterval)
989
1690
  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);
1691
+ for (const timer of this.pendingIndexRetryTimers.values()) {
1692
+ clearTimeout(timer);
999
1693
  }
1000
- // Close all processors
1001
- for (const processor of this.processors.values()) {
1002
- yield processor.close();
1694
+ this.pendingIndexRetryTimers.clear();
1695
+ this.pendingIndexRetryCounts.clear();
1696
+ // Abort explicit operation controllers, close coordinator admission, and
1697
+ // reject queued project waiters. Start these drains before watcher teardown
1698
+ // so no timer or socket can admit replacement work.
1699
+ for (const ac of this.shutdownAbortControllers) {
1700
+ ac.abort(new operation_coordinator_1.OperationClosedError());
1003
1701
  }
1702
+ const operationDrain = this.operations.close();
1703
+ const projectDrain = this.projectMutex.close();
1704
+ // Abort catchup/recovery and remove each processor before worker teardown.
1705
+ yield this.watcherManager.quiesceAll();
1706
+ yield Promise.all([operationDrain, projectDrain]);
1004
1707
  // Stop LLM server if running
1005
1708
  try {
1006
1709
  yield ((_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.stop());
1007
1710
  }
1008
- catch (_g) { }
1009
- // Stop MLX embed server if we started it
1010
- this.mlxServerManager.stopMlxServer();
1711
+ catch (_h) { }
1011
1712
  // Destroy worker pool to prevent orphaned child processes
1713
+ if (this.workerPool) {
1714
+ if (strictResourceShutdown) {
1715
+ yield this.workerPool.destroy({
1716
+ requireExit: true,
1717
+ });
1718
+ }
1719
+ else {
1720
+ try {
1721
+ yield this.workerPool.destroy();
1722
+ }
1723
+ catch (_j) { }
1724
+ }
1725
+ this.workerPool = null;
1726
+ this.resources = null;
1727
+ }
1012
1728
  if ((0, pool_1.isWorkerPoolInitialized)()) {
1013
1729
  try {
1014
1730
  yield (0, pool_1.destroyWorkerPool)();
1015
1731
  }
1016
- catch (_h) { }
1732
+ catch (_k) { }
1017
1733
  }
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();
1734
+ // Stop MLX embed server only after worker requests have drained.
1735
+ yield this.mlxServerManager.stopMlxServer();
1022
1736
  // Unregister all
1023
1737
  for (const root of this.processors.keys()) {
1024
1738
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -1027,19 +1741,27 @@ class Daemon {
1027
1741
  this.processors.clear();
1028
1742
  // Close shared resources
1029
1743
  try {
1030
- yield ((_c = this.metaCache) === null || _c === void 0 ? void 0 : _c.close());
1744
+ yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
1031
1745
  }
1032
- catch (_j) { }
1033
- try {
1034
- yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
1746
+ catch (_l) { }
1747
+ if (strictResourceShutdown) {
1748
+ yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close({ requireClosed: true }));
1749
+ }
1750
+ else {
1751
+ try {
1752
+ yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
1753
+ }
1754
+ catch (_m) { }
1035
1755
  }
1036
- catch (_k) { }
1756
+ yield serverClosed;
1757
+ clearTimeout(forceCloseConnections);
1758
+ this.connections.clear();
1037
1759
  // Hand off to a successor only after every resource is released and the
1038
1760
  // liveness markers (socket/pid/lock) are already gone — so the fresh
1039
1761
  // daemon's singleton check sees a clean slate and opens LanceDB/LMDB
1040
1762
  // without contending with this exiting process.
1041
1763
  if (opts.relaunch) {
1042
- const pid = (0, daemon_launcher_1.spawnDaemon)();
1764
+ const pid = yield (0, daemon_launcher_1.spawnDaemon)();
1043
1765
  console.log(`[daemon] Spawned successor daemon${pid ? ` (PID: ${pid})` : " (spawn failed)"}`);
1044
1766
  }
1045
1767
  // Cleanly drained — drop the marker so a later start doesn't defer to a