grepmax 0.23.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +25 -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/impact.js +33 -1
  9. package/dist/commands/index.js +23 -24
  10. package/dist/commands/list.js +23 -14
  11. package/dist/commands/mcp.js +494 -162
  12. package/dist/commands/peek.js +16 -5
  13. package/dist/commands/repair.js +54 -120
  14. package/dist/commands/search-output.js +30 -9
  15. package/dist/commands/search-run.js +75 -47
  16. package/dist/commands/search-skeletons.js +28 -18
  17. package/dist/commands/search.js +45 -49
  18. package/dist/commands/serve.js +415 -373
  19. package/dist/commands/setup.js +2 -2
  20. package/dist/commands/similar.js +5 -5
  21. package/dist/commands/skeleton.js +67 -41
  22. package/dist/commands/status.js +5 -2
  23. package/dist/commands/summarize.js +6 -0
  24. package/dist/commands/surprises.js +150 -0
  25. package/dist/commands/watch.js +102 -38
  26. package/dist/config.js +3 -0
  27. package/dist/eval-surprising-connections.js +191 -0
  28. package/dist/index.js +2 -0
  29. package/dist/lib/analysis/surprising-connections.js +600 -0
  30. package/dist/lib/daemon/daemon.js +1101 -379
  31. package/dist/lib/daemon/ipc-handler.js +122 -13
  32. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  33. package/dist/lib/daemon/process-manager.js +7 -4
  34. package/dist/lib/daemon/search-handler.js +23 -9
  35. package/dist/lib/daemon/watcher-manager.js +353 -110
  36. package/dist/lib/graph/impact-rollup.js +202 -0
  37. package/dist/lib/graph/impact.js +15 -1
  38. package/dist/lib/help/agent-cheatsheet.js +1 -0
  39. package/dist/lib/index/batch-processor.js +231 -67
  40. package/dist/lib/index/embedding-generation.js +109 -0
  41. package/dist/lib/index/embedding-status.js +23 -0
  42. package/dist/lib/index/file-policy.js +273 -0
  43. package/dist/lib/index/ignore-patterns.js +4 -0
  44. package/dist/lib/index/index-config.js +18 -4
  45. package/dist/lib/index/syncer.js +256 -79
  46. package/dist/lib/index/walker.js +66 -86
  47. package/dist/lib/index/watcher-batch.js +9 -0
  48. package/dist/lib/index/watcher.js +129 -3
  49. package/dist/lib/llm/server.js +6 -0
  50. package/dist/lib/search/searcher.js +30 -19
  51. package/dist/lib/skeleton/skeletonizer.js +118 -0
  52. package/dist/lib/skeleton/summary-formatter.js +8 -1
  53. package/dist/lib/store/store-lease.js +473 -0
  54. package/dist/lib/store/vector-db.js +302 -57
  55. package/dist/lib/utils/blocked-roots.js +63 -0
  56. package/dist/lib/utils/cross-project.js +7 -3
  57. package/dist/lib/utils/daemon-client.js +24 -1
  58. package/dist/lib/utils/daemon-launcher.js +38 -13
  59. package/dist/lib/utils/doctor-status.js +76 -0
  60. package/dist/lib/utils/file-utils.js +74 -4
  61. package/dist/lib/utils/keyed-mutex.js +101 -0
  62. package/dist/lib/utils/logger.js +57 -1
  63. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  64. package/dist/lib/utils/operation-coordinator.js +146 -0
  65. package/dist/lib/utils/path-containment.js +106 -0
  66. package/dist/lib/utils/process.js +44 -0
  67. package/dist/lib/utils/project-registry.js +351 -3
  68. package/dist/lib/utils/scope-filter.js +3 -9
  69. package/dist/lib/utils/stale-hint.js +12 -19
  70. package/dist/lib/utils/watcher-launcher.js +5 -1
  71. package/dist/lib/utils/watcher-store.js +2 -2
  72. package/dist/lib/workers/colbert-math.js +15 -12
  73. package/dist/lib/workers/embeddings/colbert.js +3 -2
  74. package/dist/lib/workers/embeddings/granite.js +4 -3
  75. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  76. package/dist/lib/workers/orchestrator.js +39 -8
  77. package/dist/lib/workers/pool.js +150 -83
  78. package/dist/lib/workers/process-child.js +11 -2
  79. package/dist/lib/workers/serialized-handler.js +10 -0
  80. package/dist/lib/workers/worker.js +13 -4
  81. package/mlx-embed-server/server.py +21 -3
  82. package/mlx-embed-server/summarizer.py +8 -0
  83. package/package.json +6 -3
  84. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  85. package/plugins/grepmax/hooks/start.js +3 -170
@@ -50,17 +50,32 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
50
50
  };
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
52
  exports.WatcherManager = void 0;
53
- const fs = __importStar(require("node:fs"));
54
53
  const path = __importStar(require("node:path"));
55
54
  const watcher = __importStar(require("@parcel/watcher"));
56
55
  const batch_processor_1 = require("../index/batch-processor");
56
+ const file_policy_1 = require("../index/file-policy");
57
+ const walker_1 = require("../index/walker");
57
58
  const watcher_1 = require("../index/watcher");
59
+ const file_utils_1 = require("../utils/file-utils");
58
60
  const logger_1 = require("../utils/logger");
59
61
  const project_registry_1 = require("../utils/project-registry");
60
62
  const watcher_store_1 = require("../utils/watcher-store");
61
63
  // Watcher health windows used for FSEvents auto-recovery.
62
64
  const FSEVENTS_RECOVERY_INTERVAL_MS = 60 * 60 * 1000; // try recovery hourly
63
65
  const FSEVENTS_HEALTH_WINDOW_MS = 5 * 60 * 1000; // 5 min of quiet = "healthy"
66
+ function abortableDelay(ms, signal) {
67
+ if (signal.aborted)
68
+ return Promise.resolve();
69
+ return new Promise((resolve) => {
70
+ const timer = setTimeout(finish, ms);
71
+ function finish() {
72
+ clearTimeout(timer);
73
+ signal.removeEventListener("abort", finish);
74
+ resolve();
75
+ }
76
+ signal.addEventListener("abort", finish, { once: true });
77
+ });
78
+ }
64
79
  /**
65
80
  * Owns per-project file watching: @parcel/watcher subscriptions, FSEvents
66
81
  * overflow recovery, poll-mode fallback, and the offline catchup scan. Holds the
@@ -78,93 +93,161 @@ class WatcherManager {
78
93
  this.watcherFailCount = new Map();
79
94
  this.pollIntervals = new Map();
80
95
  this.pollRecoveryTimers = new Map();
96
+ this.recoveryTimeouts = new Map();
81
97
  this.lastOverflowMs = new Map();
82
98
  this.lastCatchupEndMs = new Map();
99
+ this.catchups = new Map();
100
+ this.degradedRoots = new Set();
101
+ this.terminalFailures = new Map();
102
+ this.watchLifecycles = new Map();
83
103
  }
84
- watchProject(root) {
85
- return __awaiter(this, void 0, void 0, function* () {
104
+ watchProject(root_1) {
105
+ return __awaiter(this, arguments, void 0, function* (root, options = {}) {
106
+ var _a, _b;
86
107
  if (this.deps.processors.has(root) || this.pendingOps.has(root))
87
108
  return;
88
109
  const vectorDb = this.deps.getVectorDb();
89
110
  const metaCache = this.deps.getMetaCache();
90
- if (!vectorDb || !metaCache)
111
+ const workerPool = (_b = (_a = this.deps).getWorkerPool) === null || _b === void 0 ? void 0 : _b.call(_a);
112
+ if (!vectorDb || !metaCache || (this.deps.getWorkerPool && !workerPool))
91
113
  return;
92
114
  this.pendingOps.add(root);
93
- const processor = new batch_processor_1.ProjectBatchProcessor({
94
- projectRoot: root,
95
- vectorDb,
96
- metaCache,
97
- onReindex: (files, ms) => __awaiter(this, void 0, void 0, function* () {
98
- console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
99
- // Update project registry so gmax status shows fresh data
100
- const proj = (0, project_registry_1.getProject)(root);
101
- if (proj) {
102
- let chunkCount = proj.chunkCount;
103
- try {
104
- chunkCount = yield vectorDb.countRowsForPath(root);
105
- }
106
- catch (err) {
107
- console.warn(`[daemon:${path.basename(root)}] Failed to query chunk count: ${err}`);
115
+ const lifecycle = new AbortController();
116
+ this.watchLifecycles.set(root, lifecycle);
117
+ const ownsLifecycle = () => this.watchLifecycles.get(root) === lifecycle &&
118
+ !lifecycle.signal.aborted &&
119
+ !this.deps.getShuttingDown();
120
+ try {
121
+ const filePolicy = new file_policy_1.ProjectFilePolicy(root);
122
+ let processor;
123
+ processor = new batch_processor_1.ProjectBatchProcessor(Object.assign(Object.assign({ projectRoot: root, vectorDb,
124
+ metaCache }, (workerPool ? { workerPool } : {})), { filePolicy, onPolicyChange: () => {
125
+ void this.runCatchup(root, processor).catch((err) => {
126
+ console.error(`[daemon:${path.basename(root)}] Policy reconciliation failed:`, err);
127
+ });
128
+ }, onReindex: (files, ms) => __awaiter(this, void 0, void 0, function* () {
129
+ console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
130
+ // Update project registry so gmax status shows fresh data
131
+ const proj = (0, project_registry_1.getProject)(root);
132
+ if (proj) {
133
+ let chunkCount = proj.chunkCount;
134
+ try {
135
+ chunkCount = yield vectorDb.countRowsForPath(root);
136
+ }
137
+ catch (err) {
138
+ console.warn(`[daemon:${path.basename(root)}] Failed to query chunk count: ${err}`);
139
+ }
140
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount }));
108
141
  }
109
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount }));
110
- }
111
- // Back to watching after batch completes
112
- (0, watcher_store_1.registerWatcher)({
113
- pid: process.pid,
114
- projectRoot: root,
115
- startTime: Date.now(),
116
- status: "watching",
117
- lastHeartbeat: Date.now(),
118
- lastReindex: Date.now(),
119
- });
120
- }),
121
- onActivity: () => {
122
- this.deps.touchActivity();
123
- // Mark as syncing while processing
124
- (0, watcher_store_1.registerWatcher)({
125
- pid: process.pid,
126
- projectRoot: root,
127
- startTime: Date.now(),
128
- status: "syncing",
129
- lastHeartbeat: Date.now(),
142
+ // Back to watching after batch completes
143
+ (0, watcher_store_1.registerWatcher)({
144
+ pid: process.pid,
145
+ projectRoot: root,
146
+ startTime: Date.now(),
147
+ status: this.isRootDegraded(root) ? "degraded" : "watching",
148
+ lastHeartbeat: Date.now(),
149
+ lastReindex: Date.now(),
150
+ });
151
+ }), onActivity: () => {
152
+ this.deps.touchActivity();
153
+ // Mark as syncing while processing
154
+ (0, watcher_store_1.registerWatcher)({
155
+ pid: process.pid,
156
+ projectRoot: root,
157
+ startTime: Date.now(),
158
+ status: this.isRootDegraded(root) ? "degraded" : "syncing",
159
+ lastHeartbeat: Date.now(),
160
+ });
161
+ }, onTerminalFailure: (absPath) => {
162
+ var _a;
163
+ const failed = (_a = this.terminalFailures.get(root)) !== null && _a !== void 0 ? _a : new Set();
164
+ failed.add(absPath);
165
+ this.terminalFailures.set(root, failed);
166
+ (0, watcher_store_1.registerWatcher)({
167
+ pid: process.pid,
168
+ projectRoot: root,
169
+ startTime: Date.now(),
170
+ status: "degraded",
171
+ lastError: `${failed.size} file(s) exhausted automatic retries`,
172
+ lastHeartbeat: Date.now(),
173
+ });
174
+ }, onPathSuccess: (absPath) => {
175
+ const failed = this.terminalFailures.get(root);
176
+ if (!(failed === null || failed === void 0 ? void 0 : failed.delete(absPath)))
177
+ return;
178
+ if (failed.size === 0)
179
+ this.terminalFailures.delete(root);
180
+ (0, watcher_store_1.registerWatcher)({
181
+ pid: process.pid,
182
+ projectRoot: root,
183
+ startTime: Date.now(),
184
+ status: this.isRootDegraded(root) ? "degraded" : "watching",
185
+ lastHeartbeat: Date.now(),
186
+ });
187
+ }, runOperation: (fn) => this.deps.runProjectOperation
188
+ ? this.deps.runProjectOperation(root, "watch-batch", undefined, fn)
189
+ : fn(new AbortController().signal) }));
190
+ if (!ownsLifecycle()) {
191
+ yield processor.close();
192
+ return;
193
+ }
194
+ this.deps.processors.set(root, processor);
195
+ // Subscribe with @parcel/watcher — native backend, no polling.
196
+ // If the kernel refuses (e.g. FSEvents slots stuck after a prior kill -9),
197
+ // fall straight through to poll mode. The retry/backoff path inside
198
+ // recoverWatcher is for transient overflows, not hard kernel-level
199
+ // subscribe failures, so we skip it on startup by priming failCount past
200
+ // MAX before invoking it.
201
+ try {
202
+ yield this.subscribeWatcher(root, processor);
203
+ }
204
+ catch (err) {
205
+ if (!ownsLifecycle())
206
+ return;
207
+ const name = path.basename(root);
208
+ console.error(`[daemon:${name}] Subscribe failed at startup (${err instanceof Error ? err.message : err}) — switching to poll mode`);
209
+ this.watcherFailCount.set(root, 1000); // > MAX_WATCHER_RETRIES
210
+ this.lastOverflowMs.set(root, Date.now());
211
+ this.recoverWatcher(root, processor);
212
+ }
213
+ if (!ownsLifecycle())
214
+ return;
215
+ (0, watcher_store_1.registerWatcher)({
216
+ pid: process.pid,
217
+ projectRoot: root,
218
+ startTime: Date.now(),
219
+ status: processor.progress.processing || processor.progress.pendingFiles > 0
220
+ ? "syncing"
221
+ : "watching",
222
+ lastHeartbeat: Date.now(),
223
+ });
224
+ // Catchup scan — find files changed while daemon was offline
225
+ if (options.catchup !== false) {
226
+ this.runCatchup(root, processor).catch((err) => {
227
+ console.error(`[daemon:${path.basename(root)}] Catchup scan failed:`, err);
130
228
  });
131
- },
132
- });
133
- this.deps.processors.set(root, processor);
134
- // Subscribe with @parcel/watcher — native backend, no polling.
135
- // If the kernel refuses (e.g. FSEvents slots stuck after a prior kill -9),
136
- // fall straight through to poll mode. The retry/backoff path inside
137
- // recoverWatcher is for transient overflows, not hard kernel-level
138
- // subscribe failures, so we skip it on startup by priming failCount past
139
- // MAX before invoking it.
140
- try {
141
- yield this.subscribeWatcher(root, processor);
229
+ }
230
+ console.log(`[daemon] Watching ${root}`);
231
+ }
232
+ catch (error) {
233
+ yield this.unwatchProject(root).catch(() => { });
234
+ throw error;
142
235
  }
143
- catch (err) {
144
- const name = path.basename(root);
145
- console.error(`[daemon:${name}] Subscribe failed at startup (${err instanceof Error ? err.message : err}) — switching to poll mode`);
146
- this.watcherFailCount.set(root, 1000); // > MAX_WATCHER_RETRIES
147
- this.lastOverflowMs.set(root, Date.now());
148
- this.recoverWatcher(root, processor);
236
+ finally {
237
+ this.pendingOps.delete(root);
149
238
  }
150
- (0, watcher_store_1.registerWatcher)({
151
- pid: process.pid,
152
- projectRoot: root,
153
- startTime: Date.now(),
154
- status: "watching",
155
- lastHeartbeat: Date.now(),
156
- });
157
- // Catchup scan — find files changed while daemon was offline
158
- this.catchupScan(root, processor).catch((err) => {
159
- console.error(`[daemon:${path.basename(root)}] Catchup scan failed:`, err);
160
- });
161
- this.pendingOps.delete(root);
162
- console.log(`[daemon] Watching ${root}`);
163
239
  });
164
240
  }
241
+ isRootDegraded(root) {
242
+ var _a, _b;
243
+ return (this.degradedRoots.has(root) ||
244
+ ((_b = (_a = this.terminalFailures.get(root)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0) > 0);
245
+ }
165
246
  subscribeWatcher(root, processor) {
166
247
  return __awaiter(this, void 0, void 0, function* () {
167
248
  const name = path.basename(root);
249
+ if (this.deps.processors.get(root) !== processor)
250
+ return;
168
251
  // Unsubscribe existing watcher if any (e.g. during recovery)
169
252
  const existingSub = this.deps.subscriptions.get(root);
170
253
  if (existingSub) {
@@ -191,13 +274,18 @@ class WatcherManager {
191
274
  }
192
275
  this.deps.touchActivity();
193
276
  }, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
277
+ if (this.deps.processors.get(root) !== processor) {
278
+ yield sub.unsubscribe();
279
+ return;
280
+ }
194
281
  this.deps.subscriptions.set(root, sub);
195
282
  });
196
283
  }
197
284
  recoverWatcher(root, processor) {
198
285
  var _a;
199
286
  const name = path.basename(root);
200
- if (this.deps.getShuttingDown())
287
+ if (this.deps.getShuttingDown() ||
288
+ this.deps.processors.get(root) !== processor)
201
289
  return;
202
290
  // Debounce: avoid multiple overlapping recovery attempts
203
291
  const recoveryKey = `recover:${root}`;
@@ -222,14 +310,15 @@ class WatcherManager {
222
310
  if (!this.pollIntervals.has(root)) {
223
311
  console.error(`[daemon:${name}] FSEvents unreliable after ${fails} failures — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
224
312
  // Run an immediate catchup, then schedule periodic ones
225
- this.catchupScan(root, processor).catch((err) => {
313
+ this.runCatchup(root, processor).catch((err) => {
226
314
  console.error(`[daemon:${name}] Poll catchup failed:`, err);
227
315
  });
228
316
  const interval = setInterval(() => {
229
- if (this.deps.getShuttingDown())
317
+ if (this.deps.getShuttingDown() ||
318
+ this.deps.processors.get(root) !== processor)
230
319
  return;
231
320
  this.deps.touchActivity();
232
- this.catchupScan(root, processor).catch((err) => {
321
+ this.runCatchup(root, processor).catch((err) => {
233
322
  console.error(`[daemon:${name}] Poll catchup failed:`, err);
234
323
  });
235
324
  }, POLL_INTERVAL_MS);
@@ -252,8 +341,10 @@ class WatcherManager {
252
341
  // Backoff: wait before re-subscribing (3s, 6s, 12s)
253
342
  const delayMs = 3000 * Math.pow(2, (fails - 1));
254
343
  console.error(`[daemon:${name}] Recovering watcher (attempt ${fails}/${MAX_WATCHER_RETRIES}, backoff ${delayMs}ms)...`);
255
- setTimeout(() => {
256
- if (this.deps.getShuttingDown()) {
344
+ const timeout = setTimeout(() => {
345
+ this.recoveryTimeouts.delete(root);
346
+ if (this.deps.getShuttingDown() ||
347
+ this.deps.processors.get(root) !== processor) {
257
348
  this.pendingOps.delete(recoveryKey);
258
349
  return;
259
350
  }
@@ -261,13 +352,15 @@ class WatcherManager {
261
352
  var _a;
262
353
  try {
263
354
  yield this.subscribeWatcher(root, processor);
355
+ if (this.deps.processors.get(root) !== processor)
356
+ return;
264
357
  const lastCatchup = (_a = this.lastCatchupEndMs.get(root)) !== null && _a !== void 0 ? _a : 0;
265
358
  const CATCHUP_COOLDOWN_MS = 60000;
266
359
  if (Date.now() - lastCatchup < CATCHUP_COOLDOWN_MS) {
267
360
  console.log(`[daemon:${name}] Skipping catchup scan (last completed ${Math.round((Date.now() - lastCatchup) / 1000)}s ago)`);
268
361
  }
269
362
  else {
270
- yield this.catchupScan(root, processor);
363
+ yield this.runCatchup(root, processor);
271
364
  }
272
365
  console.log(`[daemon:${name}] Watcher recovered`);
273
366
  }
@@ -279,6 +372,7 @@ class WatcherManager {
279
372
  }
280
373
  }))();
281
374
  }, delayMs);
375
+ this.recoveryTimeouts.set(root, timeout);
282
376
  }
283
377
  /**
284
378
  * Once a project has fallen back to poll mode, periodically try to upgrade
@@ -290,6 +384,9 @@ class WatcherManager {
290
384
  if (this.pollRecoveryTimers.has(root))
291
385
  return;
292
386
  const name = path.basename(root);
387
+ const lifecycle = this.watchLifecycles.get(root);
388
+ if (!lifecycle)
389
+ return;
293
390
  const timer = setInterval(() => {
294
391
  if (this.deps.getShuttingDown())
295
392
  return;
@@ -305,6 +402,8 @@ class WatcherManager {
305
402
  return;
306
403
  void (() => __awaiter(this, void 0, void 0, function* () {
307
404
  var _a;
405
+ if (this.deps.processors.get(root) !== processor)
406
+ return;
308
407
  console.log(`[daemon:${name}] Attempting to leave poll mode and reattach FSEvents...`);
309
408
  try {
310
409
  // Reset failure counter so subscribeWatcher's error path treats this
@@ -312,10 +411,13 @@ class WatcherManager {
312
411
  // poll mode via the same recoverWatcher path.
313
412
  this.watcherFailCount.delete(root);
314
413
  yield this.subscribeWatcher(root, processor);
414
+ if (this.deps.processors.get(root) !== processor)
415
+ return;
315
416
  // Wait one health window — if the new subscription survives without
316
417
  // another overflow, we consider it recovered and tear down poll mode.
317
- yield new Promise((r) => setTimeout(r, FSEVENTS_HEALTH_WINDOW_MS));
318
- if (this.deps.getShuttingDown())
418
+ yield abortableDelay(FSEVENTS_HEALTH_WINDOW_MS, lifecycle.signal);
419
+ if (this.deps.getShuttingDown() ||
420
+ this.deps.processors.get(root) !== processor)
319
421
  return;
320
422
  const lastOverflow = (_a = this.lastOverflowMs.get(root)) !== null && _a !== void 0 ? _a : 0;
321
423
  if (Date.now() - lastOverflow < FSEVENTS_HEALTH_WINDOW_MS) {
@@ -343,50 +445,65 @@ class WatcherManager {
343
445
  timer.unref();
344
446
  this.pollRecoveryTimers.set(root, timer);
345
447
  }
346
- catchupScan(root, processor) {
448
+ catchupScan(root, processor, signal) {
347
449
  return __awaiter(this, void 0, void 0, function* () {
348
450
  var _a, e_1, _b, _c;
349
- const { walk } = yield Promise.resolve().then(() => __importStar(require("../index/walker")));
350
- const { INDEXABLE_EXTENSIONS, MAX_FILE_SIZE_BYTES } = yield Promise.resolve().then(() => __importStar(require("../../config")));
451
+ var _d, _e;
351
452
  const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
352
453
  const metaCache = this.deps.getMetaCache();
454
+ processor.filePolicy.invalidateIgnoreCache();
353
455
  const rootPrefix = root.endsWith("/") ? root : `${root}/`;
354
456
  const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
457
+ if (signal.aborted)
458
+ return false;
355
459
  const seenPaths = new Set();
460
+ const walkState = (0, walker_1.createWalkState)();
356
461
  let queued = 0;
357
462
  let skipped = 0;
358
463
  let debugSamples = 0;
359
464
  try {
360
- for (var _d = true, _e = __asyncValues(walk(root, {
361
- additionalPatterns: ["**/.git/**", "**/.gmax/**"],
362
- })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
363
- _c = _f.value;
364
- _d = false;
465
+ for (var _f = true, _g = __asyncValues((0, walker_1.walk)(root, {
466
+ policy: processor.filePolicy,
467
+ state: walkState,
468
+ })), _h; _h = yield _g.next(), _a = _h.done, !_a; _f = true) {
469
+ _c = _h.value;
470
+ _f = false;
365
471
  const relPath = _c;
472
+ if (signal.aborted)
473
+ return false;
366
474
  const absPath = path.join(root, relPath);
367
- const ext = path.extname(absPath).toLowerCase();
368
- const bn = path.basename(absPath).toLowerCase();
369
- if (!INDEXABLE_EXTENSIONS.has(ext) && !INDEXABLE_EXTENSIONS.has(bn))
370
- continue;
371
475
  try {
372
- const stats = yield fs.promises.stat(absPath);
373
- // Skip files that are too large or empty — they'll never be indexed.
374
- // Leave them OUT of seenPaths so a file that became empty/oversized is
375
- // treated as deleted by the purge sweep below and its now-stale chunks
376
- // are removed, rather than lingering as unsearchable-but-present rows.
377
- if (stats.size === 0 || stats.size > MAX_FILE_SIZE_BYTES)
476
+ const classification = yield processor.filePolicy.classifyFile(absPath);
477
+ if (classification.status === "error") {
478
+ walkState.protectedPaths.add(classification.protectedPath);
479
+ walkState.errors.push({
480
+ path: classification.protectedPath,
481
+ error: classification.error,
482
+ });
378
483
  continue;
484
+ }
485
+ if (classification.status !== "indexable")
486
+ continue;
487
+ const stats = classification.stat;
379
488
  seenPaths.add(absPath);
380
489
  const cached = metaCache.get(absPath);
381
490
  if (!isFileCached(cached, stats)) {
382
491
  // Fast path: if only mtime changed but size is identical and we have a hash,
383
492
  // just verify the hash in-process instead of sending to a worker.
384
493
  if ((cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
385
- const { computeContentHash } = yield Promise.resolve().then(() => __importStar(require("../utils/file-utils")));
386
- const buf = yield fs.promises.readFile(absPath);
387
- const hash = computeContentHash(buf, absPath);
494
+ const snapshot = yield (0, file_utils_1.readFileSnapshot)(absPath, {
495
+ projectRoot: root,
496
+ });
497
+ if (snapshot.size !== stats.size ||
498
+ snapshot.mtimeMs !== stats.mtimeMs) {
499
+ processor.handleFileEvent("change", absPath);
500
+ continue;
501
+ }
502
+ const hash = (0, file_utils_1.computeContentHash)(snapshot.buffer, absPath);
388
503
  if (hash === cached.hash) {
389
504
  // Content unchanged — update mtime in cache and skip worker
505
+ if (signal.aborted)
506
+ return false;
390
507
  metaCache.put(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: stats.mtimeMs }));
391
508
  skipped++;
392
509
  continue;
@@ -397,26 +514,31 @@ class WatcherManager {
397
514
  (0, logger_1.debug)("catchup", `miss ${relPath}: cached=${cached ? `mtime=${Math.trunc(cached.mtimeMs)} size=${cached.size}` : "null"} stat=mtime=${Math.trunc(stats.mtimeMs)} size=${stats.size}`);
398
515
  debugSamples++;
399
516
  }
517
+ if (signal.aborted)
518
+ return false;
400
519
  processor.handleFileEvent("change", absPath);
401
520
  queued++;
402
521
  // Throttle: pause periodically during large catchup scans to let the
403
522
  // batch processor drain and compaction run between bursts.
404
523
  if (queued % 500 === 0) {
405
524
  (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
406
- yield new Promise((r) => setTimeout(r, 5000));
525
+ yield abortableDelay(5000, signal);
407
526
  }
408
527
  }
409
528
  else {
410
529
  skipped++;
411
530
  }
412
531
  }
413
- catch (_g) { }
532
+ catch (error) {
533
+ walkState.protectedPaths.add(absPath);
534
+ walkState.errors.push({ path: absPath, error });
535
+ }
414
536
  }
415
537
  }
416
538
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
417
539
  finally {
418
540
  try {
419
- if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
541
+ if (!_f && !_a && (_b = _g.return)) yield _b.call(_g);
420
542
  }
421
543
  finally { if (e_1) throw e_1.error; }
422
544
  }
@@ -424,11 +546,33 @@ class WatcherManager {
424
546
  // Purge files deleted while daemon was offline
425
547
  let purged = 0;
426
548
  for (const cachedPath of cachedPaths) {
427
- if (!seenPaths.has(cachedPath)) {
549
+ if (signal.aborted)
550
+ return false;
551
+ if (!seenPaths.has(cachedPath) &&
552
+ !(0, walker_1.isPathProtectedByWalkState)(cachedPath, walkState)) {
428
553
  processor.handleFileEvent("unlink", cachedPath);
429
554
  purged++;
430
555
  }
431
556
  }
557
+ const complete = walkState.rootComplete && walkState.errors.length === 0;
558
+ if (!complete) {
559
+ this.degradedRoots.add(root);
560
+ (0, watcher_store_1.registerWatcher)({
561
+ pid: process.pid,
562
+ projectRoot: root,
563
+ startTime: Date.now(),
564
+ status: "degraded",
565
+ lastHeartbeat: Date.now(),
566
+ lastError: `${walkState.errors.length} scan path(s) incomplete`,
567
+ });
568
+ }
569
+ else {
570
+ this.degradedRoots.delete(root);
571
+ const failedFiles = (_e = (_d = this.terminalFailures.get(root)) === null || _d === void 0 ? void 0 : _d.size) !== null && _e !== void 0 ? _e : 0;
572
+ (0, watcher_store_1.registerWatcher)(Object.assign({ pid: process.pid, projectRoot: root, startTime: Date.now(), status: this.isRootDegraded(root) ? "degraded" : "watching", lastHeartbeat: Date.now() }, (failedFiles > 0
573
+ ? { lastError: `${failedFiles} file(s) exhausted automatic retries` }
574
+ : {})));
575
+ }
432
576
  if (queued > 0 || purged > 0) {
433
577
  const parts = [];
434
578
  if (queued > 0)
@@ -438,13 +582,62 @@ class WatcherManager {
438
582
  console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
439
583
  }
440
584
  this.lastCatchupEndMs.set(root, Date.now());
585
+ return complete;
586
+ });
587
+ }
588
+ runCatchup(root, processor) {
589
+ const existing = this.catchups.get(root);
590
+ if (existing) {
591
+ existing.dirty = true;
592
+ return existing.promise;
593
+ }
594
+ const active = {
595
+ controller: new AbortController(),
596
+ dirty: false,
597
+ promise: Promise.resolve(),
598
+ };
599
+ const execute = (signal) => __awaiter(this, void 0, void 0, function* () {
600
+ let incompleteRetries = 0;
601
+ do {
602
+ active.dirty = false;
603
+ const complete = yield this.catchupScan(root, processor, signal);
604
+ if (!complete && !signal.aborted && incompleteRetries < 3) {
605
+ incompleteRetries++;
606
+ active.dirty = true;
607
+ yield abortableDelay(1000 * Math.pow(2, (incompleteRetries - 1)), signal);
608
+ }
609
+ else if (complete) {
610
+ incompleteRetries = 0;
611
+ }
612
+ } while (active.dirty && !signal.aborted);
613
+ });
614
+ const run = (this.deps.runProjectOperation
615
+ ? this.deps.runProjectOperation(root, "watch-catchup", active.controller.signal, execute)
616
+ : execute(active.controller.signal)).finally(() => {
617
+ if (this.catchups.get(root) === active)
618
+ this.catchups.delete(root);
619
+ });
620
+ active.promise = run;
621
+ this.catchups.set(root, active);
622
+ return run;
623
+ }
624
+ stopCatchup(root) {
625
+ return __awaiter(this, void 0, void 0, function* () {
626
+ const active = this.catchups.get(root);
627
+ if (!active)
628
+ return;
629
+ active.controller.abort();
630
+ yield active.promise.catch(() => { });
441
631
  });
442
632
  }
443
633
  unwatchProject(root) {
444
634
  return __awaiter(this, void 0, void 0, function* () {
635
+ var _a;
445
636
  // Stop poll-mode timers + their FSEvents recovery probe first, so a removed
446
637
  // project can't keep scanning until full daemon shutdown. These live
447
638
  // independently of the processor, so clear them even on the early return.
639
+ (_a = this.watchLifecycles.get(root)) === null || _a === void 0 ? void 0 : _a.abort();
640
+ this.watchLifecycles.delete(root);
448
641
  const pollInterval = this.pollIntervals.get(root);
449
642
  if (pollInterval) {
450
643
  clearInterval(pollInterval);
@@ -455,30 +648,72 @@ class WatcherManager {
455
648
  clearInterval(recoveryTimer);
456
649
  this.pollRecoveryTimers.delete(root);
457
650
  }
651
+ const recoveryTimeout = this.recoveryTimeouts.get(root);
652
+ if (recoveryTimeout) {
653
+ clearTimeout(recoveryTimeout);
654
+ this.recoveryTimeouts.delete(root);
655
+ }
656
+ this.pendingOps.delete(`recover:${root}`);
458
657
  const processor = this.deps.processors.get(root);
459
- if (!processor)
460
- return;
461
- yield processor.close();
658
+ this.deps.processors.delete(root);
659
+ yield this.stopCatchup(root);
660
+ if (processor)
661
+ yield processor.close();
462
662
  const sub = this.deps.subscriptions.get(root);
463
663
  if (sub) {
464
664
  yield sub.unsubscribe();
465
665
  this.deps.subscriptions.delete(root);
466
666
  }
467
- this.deps.processors.delete(root);
468
667
  this.deps.evictSearcher(root);
469
668
  this.lastOverflowMs.delete(root);
470
669
  this.lastCatchupEndMs.delete(root);
670
+ this.degradedRoots.delete(root);
671
+ this.terminalFailures.delete(root);
471
672
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
472
673
  console.log(`[daemon] Unwatched ${root}`);
473
674
  });
474
675
  }
676
+ quiesceAll() {
677
+ return __awaiter(this, void 0, void 0, function* () {
678
+ const roots = [
679
+ ...new Set([
680
+ ...this.deps.processors.keys(),
681
+ ...this.deps.subscriptions.keys(),
682
+ ...this.watchLifecycles.keys(),
683
+ ]),
684
+ ];
685
+ yield this.teardown();
686
+ yield Promise.all(roots.map((root) => this.unwatchProject(root)));
687
+ return roots;
688
+ });
689
+ }
690
+ resumeAll(roots_1) {
691
+ return __awaiter(this, arguments, void 0, function* (roots, options = {}) {
692
+ if (this.deps.getShuttingDown())
693
+ return;
694
+ for (const root of roots)
695
+ yield this.watchProject(root, options);
696
+ });
697
+ }
698
+ catchupAll(roots) {
699
+ return __awaiter(this, void 0, void 0, function* () {
700
+ for (const root of roots) {
701
+ const processor = this.deps.processors.get(root);
702
+ if (processor)
703
+ yield this.runCatchup(root, processor);
704
+ }
705
+ });
706
+ }
475
707
  /**
476
708
  * Stop all poll intervals + their FSEvents recovery probes and unsubscribe
477
- * every watcher. Called from the daemon's shutdown after the worker pool is
478
- * destroyed (the subscriptions map is shared, so it is cleared here).
709
+ * every watcher. Processors are closed by quiesceAll/unwatchProject before
710
+ * worker teardown.
479
711
  */
480
712
  teardown() {
481
713
  return __awaiter(this, void 0, void 0, function* () {
714
+ for (const lifecycle of this.watchLifecycles.values())
715
+ lifecycle.abort();
716
+ this.watchLifecycles.clear();
482
717
  for (const interval of this.pollIntervals.values()) {
483
718
  clearInterval(interval);
484
719
  }
@@ -487,6 +722,14 @@ class WatcherManager {
487
722
  clearInterval(interval);
488
723
  }
489
724
  this.pollRecoveryTimers.clear();
725
+ for (const timeout of this.recoveryTimeouts.values()) {
726
+ clearTimeout(timeout);
727
+ }
728
+ this.recoveryTimeouts.clear();
729
+ const catchups = [...this.catchups.values()];
730
+ for (const active of catchups)
731
+ active.controller.abort();
732
+ yield Promise.allSettled(catchups.map((active) => active.promise));
490
733
  for (const sub of this.deps.subscriptions.values()) {
491
734
  try {
492
735
  yield sub.unsubscribe();