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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +25 -6
- package/dist/commands/add.js +38 -31
- package/dist/commands/config.js +16 -15
- package/dist/commands/context.js +38 -13
- package/dist/commands/doctor.js +76 -83
- package/dist/commands/extract.js +12 -1
- package/dist/commands/impact.js +33 -1
- package/dist/commands/index.js +23 -24
- package/dist/commands/list.js +23 -14
- package/dist/commands/mcp.js +494 -162
- package/dist/commands/peek.js +16 -5
- package/dist/commands/repair.js +54 -120
- package/dist/commands/search-output.js +30 -9
- package/dist/commands/search-run.js +75 -47
- package/dist/commands/search-skeletons.js +28 -18
- package/dist/commands/search.js +45 -49
- package/dist/commands/serve.js +415 -373
- package/dist/commands/setup.js +2 -2
- package/dist/commands/similar.js +5 -5
- package/dist/commands/skeleton.js +67 -41
- package/dist/commands/status.js +5 -2
- package/dist/commands/summarize.js +6 -0
- package/dist/commands/surprises.js +150 -0
- package/dist/commands/watch.js +102 -38
- package/dist/config.js +3 -0
- package/dist/eval-surprising-connections.js +191 -0
- package/dist/index.js +2 -0
- package/dist/lib/analysis/surprising-connections.js +600 -0
- package/dist/lib/daemon/daemon.js +1101 -379
- package/dist/lib/daemon/ipc-handler.js +122 -13
- package/dist/lib/daemon/mlx-server-manager.js +268 -125
- package/dist/lib/daemon/process-manager.js +7 -4
- package/dist/lib/daemon/search-handler.js +23 -9
- package/dist/lib/daemon/watcher-manager.js +353 -110
- package/dist/lib/graph/impact-rollup.js +202 -0
- package/dist/lib/graph/impact.js +15 -1
- package/dist/lib/help/agent-cheatsheet.js +1 -0
- package/dist/lib/index/batch-processor.js +231 -67
- package/dist/lib/index/embedding-generation.js +109 -0
- package/dist/lib/index/embedding-status.js +23 -0
- package/dist/lib/index/file-policy.js +273 -0
- package/dist/lib/index/ignore-patterns.js +4 -0
- package/dist/lib/index/index-config.js +18 -4
- package/dist/lib/index/syncer.js +256 -79
- package/dist/lib/index/walker.js +66 -86
- package/dist/lib/index/watcher-batch.js +9 -0
- package/dist/lib/index/watcher.js +129 -3
- package/dist/lib/llm/server.js +6 -0
- package/dist/lib/search/searcher.js +30 -19
- package/dist/lib/skeleton/skeletonizer.js +118 -0
- package/dist/lib/skeleton/summary-formatter.js +8 -1
- package/dist/lib/store/store-lease.js +473 -0
- package/dist/lib/store/vector-db.js +302 -57
- package/dist/lib/utils/blocked-roots.js +63 -0
- package/dist/lib/utils/cross-project.js +7 -3
- package/dist/lib/utils/daemon-client.js +24 -1
- package/dist/lib/utils/daemon-launcher.js +38 -13
- package/dist/lib/utils/doctor-status.js +76 -0
- package/dist/lib/utils/file-utils.js +74 -4
- package/dist/lib/utils/keyed-mutex.js +101 -0
- package/dist/lib/utils/logger.js +57 -1
- package/dist/lib/utils/mlx-hf-cache.js +114 -0
- package/dist/lib/utils/operation-coordinator.js +146 -0
- package/dist/lib/utils/path-containment.js +106 -0
- package/dist/lib/utils/process.js +44 -0
- package/dist/lib/utils/project-registry.js +351 -3
- package/dist/lib/utils/scope-filter.js +3 -9
- package/dist/lib/utils/stale-hint.js +12 -19
- package/dist/lib/utils/watcher-launcher.js +5 -1
- package/dist/lib/utils/watcher-store.js +2 -2
- package/dist/lib/workers/colbert-math.js +15 -12
- package/dist/lib/workers/embeddings/colbert.js +3 -2
- package/dist/lib/workers/embeddings/granite.js +4 -3
- package/dist/lib/workers/embeddings/mlx-client.js +59 -16
- package/dist/lib/workers/orchestrator.js +39 -8
- package/dist/lib/workers/pool.js +150 -83
- package/dist/lib/workers/process-child.js +11 -2
- package/dist/lib/workers/serialized-handler.js +10 -0
- package/dist/lib/workers/worker.js +13 -4
- package/mlx-embed-server/server.py +21 -3
- package/mlx-embed-server/summarizer.py +8 -0
- package/package.json +6 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- 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(
|
|
85
|
-
return __awaiter(this,
|
|
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
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
525
|
+
yield abortableDelay(5000, signal);
|
|
407
526
|
}
|
|
408
527
|
}
|
|
409
528
|
else {
|
|
410
529
|
skipped++;
|
|
411
530
|
}
|
|
412
531
|
}
|
|
413
|
-
catch (
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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.
|
|
478
|
-
*
|
|
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();
|