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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +11 -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/index.js +23 -24
- package/dist/commands/list.js +23 -14
- package/dist/commands/mcp.js +278 -143
- 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/watch.js +102 -38
- package/dist/config.js +3 -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/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/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 +146 -82
- 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 +4 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/start.js +3 -170
|
@@ -43,36 +43,44 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.ProjectBatchProcessor = void 0;
|
|
46
|
-
const fs = __importStar(require("node:fs"));
|
|
47
46
|
const path = __importStar(require("node:path"));
|
|
48
|
-
const config_1 = require("../../config");
|
|
49
47
|
const vector_db_1 = require("../store/vector-db");
|
|
50
48
|
const cache_check_1 = require("../utils/cache-check");
|
|
51
49
|
const file_utils_1 = require("../utils/file-utils");
|
|
52
50
|
const logger_1 = require("../utils/logger");
|
|
53
51
|
const pool_1 = require("../workers/pool");
|
|
52
|
+
const file_policy_1 = require("./file-policy");
|
|
54
53
|
const watcher_batch_1 = require("./watcher-batch");
|
|
55
|
-
// Fast path-segment check to reject events that leak through FSEvents overflow.
|
|
56
|
-
// Matches /node_modules/, /.git/, /dist/, /build/, /.next/, etc. anywhere in path.
|
|
57
|
-
const IGNORED_PATH_SEGMENTS_RE = /\/(?:node_modules|\.git|\.next|\.nuxt|__pycache__|coverage|\.gmax|\.venv|venv|site-packages|dist|build|out|target|vendor|\.tox|\.gradle|\.m2)\//;
|
|
58
54
|
const DEBOUNCE_MS = 2000;
|
|
59
55
|
const MAX_RETRIES = 5;
|
|
60
56
|
const MAX_BATCH_SIZE = 50;
|
|
61
57
|
class ProjectBatchProcessor {
|
|
62
58
|
constructor(opts) {
|
|
59
|
+
var _a, _b;
|
|
63
60
|
this.pending = new Map();
|
|
64
61
|
this.retryCount = new Map();
|
|
62
|
+
this.retryAt = new Map();
|
|
63
|
+
this.terminalFailures = new Set();
|
|
65
64
|
this.debounceTimer = null;
|
|
65
|
+
this.debounceDueMs = 0;
|
|
66
66
|
this.activeBatch = null;
|
|
67
67
|
this.processing = false;
|
|
68
68
|
this.closed = false;
|
|
69
69
|
this.currentBatchAc = null;
|
|
70
70
|
this.lastCorruptionLogMs = 0;
|
|
71
|
+
this.policyChangedDuringBatch = false;
|
|
71
72
|
this.projectRoot = opts.projectRoot;
|
|
72
73
|
this.vectorDb = opts.vectorDb;
|
|
73
74
|
this.metaCache = opts.metaCache;
|
|
75
|
+
this.workerPool = (_a = opts.workerPool) !== null && _a !== void 0 ? _a : (0, pool_1.getWorkerPool)();
|
|
74
76
|
this.onReindex = opts.onReindex;
|
|
75
77
|
this.onActivity = opts.onActivity;
|
|
78
|
+
this.onPolicyChange = opts.onPolicyChange;
|
|
79
|
+
this.onTerminalFailure = opts.onTerminalFailure;
|
|
80
|
+
this.onPathSuccess = opts.onPathSuccess;
|
|
81
|
+
this.runOperation = opts.runOperation;
|
|
82
|
+
this.filePolicy =
|
|
83
|
+
(_b = opts.filePolicy) !== null && _b !== void 0 ? _b : new file_policy_1.ProjectFilePolicy(opts.projectRoot);
|
|
76
84
|
this.wtag = `watch:${path.basename(opts.projectRoot)}`;
|
|
77
85
|
const taskTimeoutMs = (() => {
|
|
78
86
|
var _a;
|
|
@@ -82,24 +90,38 @@ class ProjectBatchProcessor {
|
|
|
82
90
|
this.batchTimeoutMs = Math.max(Math.ceil(taskTimeoutMs * 1.5), 120000);
|
|
83
91
|
}
|
|
84
92
|
handleFileEvent(event, absPath) {
|
|
85
|
-
var _a;
|
|
93
|
+
var _a, _b;
|
|
86
94
|
if (this.closed)
|
|
87
95
|
return;
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
96
|
+
const normalize = this.filePolicy.normalizeEventPath;
|
|
97
|
+
const normalized = normalize
|
|
98
|
+
? normalize.call(this.filePolicy, absPath)
|
|
99
|
+
: this.filePolicy.isLexicallyContained(absPath)
|
|
100
|
+
? path.resolve(absPath)
|
|
101
|
+
: null;
|
|
102
|
+
if (!normalized)
|
|
103
|
+
return;
|
|
104
|
+
if (this.filePolicy.isPolicyFile(normalized)) {
|
|
105
|
+
this.filePolicy.invalidateIgnoreCache();
|
|
106
|
+
if (this.processing)
|
|
107
|
+
this.policyChangedDuringBatch = true;
|
|
108
|
+
(_a = this.onPolicyChange) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
91
109
|
return;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (IGNORED_PATH_SEGMENTS_RE.test(absPath))
|
|
110
|
+
}
|
|
111
|
+
if (!(0, file_utils_1.isIndexableFile)(normalized, 1) && !this.metaCache.get(normalized))
|
|
95
112
|
return;
|
|
96
|
-
this.
|
|
97
|
-
|
|
113
|
+
this.retryAt.delete(normalized);
|
|
114
|
+
this.pending.set(normalized, event);
|
|
115
|
+
(_b = this.onActivity) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
98
116
|
this.scheduleBatch();
|
|
99
117
|
}
|
|
100
118
|
/** Live (re)index progress: files queued + whether a batch is running. */
|
|
101
119
|
get progress() {
|
|
102
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
pendingFiles: this.pending.size,
|
|
122
|
+
processing: this.processing,
|
|
123
|
+
failedFiles: this.terminalFailures.size,
|
|
124
|
+
};
|
|
103
125
|
}
|
|
104
126
|
close() {
|
|
105
127
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -109,24 +131,47 @@ class ProjectBatchProcessor {
|
|
|
109
131
|
if (this.debounceTimer) {
|
|
110
132
|
clearTimeout(this.debounceTimer);
|
|
111
133
|
this.debounceTimer = null;
|
|
134
|
+
this.debounceDueMs = 0;
|
|
112
135
|
}
|
|
113
136
|
if (this.activeBatch) {
|
|
114
137
|
yield this.activeBatch;
|
|
115
138
|
}
|
|
116
139
|
});
|
|
117
140
|
}
|
|
118
|
-
scheduleBatch() {
|
|
141
|
+
scheduleBatch(delayMs = DEBOUNCE_MS) {
|
|
142
|
+
const dueMs = Date.now() + Math.max(0, delayMs);
|
|
143
|
+
if (this.debounceTimer && this.debounceDueMs <= dueMs)
|
|
144
|
+
return;
|
|
119
145
|
if (this.debounceTimer)
|
|
120
146
|
clearTimeout(this.debounceTimer);
|
|
147
|
+
this.debounceDueMs = dueMs;
|
|
121
148
|
this.debounceTimer = setTimeout(() => {
|
|
122
149
|
this.debounceTimer = null;
|
|
150
|
+
this.debounceDueMs = 0;
|
|
123
151
|
this.startBatch();
|
|
124
|
-
},
|
|
152
|
+
}, Math.max(0, delayMs));
|
|
153
|
+
}
|
|
154
|
+
schedulePendingBatch(defaultDelayMs = DEBOUNCE_MS) {
|
|
155
|
+
var _a;
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
let earliestRetryMs = Number.POSITIVE_INFINITY;
|
|
158
|
+
for (const absPath of this.pending.keys()) {
|
|
159
|
+
const retryAt = (_a = this.retryAt.get(absPath)) !== null && _a !== void 0 ? _a : 0;
|
|
160
|
+
if (retryAt <= now) {
|
|
161
|
+
this.scheduleBatch(defaultDelayMs);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
earliestRetryMs = Math.min(earliestRetryMs, retryAt - now);
|
|
165
|
+
}
|
|
166
|
+
this.scheduleBatch(Number.isFinite(earliestRetryMs) ? earliestRetryMs : defaultDelayMs);
|
|
125
167
|
}
|
|
126
168
|
startBatch() {
|
|
127
169
|
if (this.activeBatch)
|
|
128
170
|
return;
|
|
129
|
-
const
|
|
171
|
+
const execute = (signal) => this.processBatch(signal);
|
|
172
|
+
const run = (this.runOperation
|
|
173
|
+
? this.runOperation(execute)
|
|
174
|
+
: execute(new AbortController().signal)).catch((err) => {
|
|
130
175
|
console.error(`[${this.wtag}] Batch processing failed:`, err);
|
|
131
176
|
});
|
|
132
177
|
this.activeBatch = run;
|
|
@@ -135,9 +180,9 @@ class ProjectBatchProcessor {
|
|
|
135
180
|
this.activeBatch = null;
|
|
136
181
|
});
|
|
137
182
|
}
|
|
138
|
-
processBatch() {
|
|
183
|
+
processBatch(operationSignal) {
|
|
139
184
|
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
-
var _a;
|
|
185
|
+
var _a, _b, _c, _d, _e;
|
|
141
186
|
if (this.closed || this.processing || this.pending.size === 0)
|
|
142
187
|
return;
|
|
143
188
|
// Circuit breaker: don't attempt writes when disk is critically low
|
|
@@ -145,29 +190,44 @@ class ProjectBatchProcessor {
|
|
|
145
190
|
(0, logger_1.log)(this.wtag, "Disk critically low — deferring batch processing");
|
|
146
191
|
if (this.debounceTimer)
|
|
147
192
|
clearTimeout(this.debounceTimer);
|
|
193
|
+
this.debounceDueMs = Date.now() + 60000;
|
|
148
194
|
this.debounceTimer = setTimeout(() => {
|
|
149
195
|
this.debounceTimer = null;
|
|
196
|
+
this.debounceDueMs = 0;
|
|
150
197
|
this.startBatch();
|
|
151
198
|
}, 60000);
|
|
152
199
|
return;
|
|
153
200
|
}
|
|
154
|
-
this.processing = true;
|
|
155
|
-
const batchAc = new AbortController();
|
|
156
|
-
this.currentBatchAc = batchAc;
|
|
157
|
-
const batchTimeout = setTimeout(() => {
|
|
158
|
-
(0, logger_1.log)(this.wtag, `Batch timed out after ${this.batchTimeoutMs}ms, aborting`);
|
|
159
|
-
batchAc.abort();
|
|
160
|
-
}, this.batchTimeoutMs);
|
|
161
201
|
const batch = new Map();
|
|
202
|
+
const now = Date.now();
|
|
162
203
|
let taken = 0;
|
|
163
204
|
for (const [absPath, event] of this.pending) {
|
|
205
|
+
if (((_a = this.retryAt.get(absPath)) !== null && _a !== void 0 ? _a : 0) > now)
|
|
206
|
+
continue;
|
|
164
207
|
batch.set(absPath, event);
|
|
165
208
|
taken++;
|
|
166
209
|
if (taken >= MAX_BATCH_SIZE)
|
|
167
210
|
break;
|
|
168
211
|
}
|
|
212
|
+
if (batch.size === 0) {
|
|
213
|
+
this.schedulePendingBatch(30000);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.processing = true;
|
|
217
|
+
const batchAc = new AbortController();
|
|
218
|
+
const abortBatch = () => batchAc.abort(operationSignal.reason);
|
|
219
|
+
if (operationSignal.aborted)
|
|
220
|
+
abortBatch();
|
|
221
|
+
else
|
|
222
|
+
operationSignal.addEventListener("abort", abortBatch, { once: true });
|
|
223
|
+
this.currentBatchAc = batchAc;
|
|
224
|
+
const batchTimeout = setTimeout(() => {
|
|
225
|
+
(0, logger_1.log)(this.wtag, `Batch timed out after ${this.batchTimeoutMs}ms, aborting`);
|
|
226
|
+
batchAc.abort();
|
|
227
|
+
}, this.batchTimeoutMs);
|
|
169
228
|
for (const key of batch.keys()) {
|
|
170
229
|
this.pending.delete(key);
|
|
230
|
+
this.retryAt.delete(key);
|
|
171
231
|
}
|
|
172
232
|
const filenames = [...batch.keys()].map((p) => path.basename(p));
|
|
173
233
|
(0, logger_1.log)(this.wtag, `Processing ${batch.size} changed files: ${filenames.join(", ")}`);
|
|
@@ -177,12 +237,32 @@ class ProjectBatchProcessor {
|
|
|
177
237
|
let backoffOverrideMs = 0;
|
|
178
238
|
try {
|
|
179
239
|
// No lock needed — daemon is the single writer to LanceDB/MetaCache
|
|
180
|
-
const pool =
|
|
240
|
+
const pool = this.workerPool;
|
|
181
241
|
const deletes = [];
|
|
182
242
|
const vectors = [];
|
|
183
243
|
const metaUpdates = new Map();
|
|
184
244
|
const metaDeletes = [];
|
|
185
245
|
const completed = new Set();
|
|
246
|
+
const retryFailures = new Set();
|
|
247
|
+
const requeuePath = (absPath, event, failed) => {
|
|
248
|
+
var _a, _b;
|
|
249
|
+
if (this.pending.has(absPath))
|
|
250
|
+
return;
|
|
251
|
+
if (failed) {
|
|
252
|
+
const retry = (0, watcher_batch_1.computePathRetry)((_a = this.retryCount.get(absPath)) !== null && _a !== void 0 ? _a : 0, MAX_RETRIES, DEBOUNCE_MS);
|
|
253
|
+
if (!retry.retry) {
|
|
254
|
+
(0, logger_1.log)(this.wtag, `Dropped ${path.basename(absPath)} after ${MAX_RETRIES} retries`);
|
|
255
|
+
this.retryCount.delete(absPath);
|
|
256
|
+
this.retryAt.delete(absPath);
|
|
257
|
+
this.terminalFailures.add(absPath);
|
|
258
|
+
(_b = this.onTerminalFailure) === null || _b === void 0 ? void 0 : _b.call(this, absPath);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.retryCount.set(absPath, retry.failures);
|
|
262
|
+
this.retryAt.set(absPath, Date.now() + retry.backoffMs);
|
|
263
|
+
}
|
|
264
|
+
this.pending.set(absPath, event);
|
|
265
|
+
};
|
|
186
266
|
for (const [absPath, event] of batch) {
|
|
187
267
|
if (batchAc.signal.aborted)
|
|
188
268
|
break;
|
|
@@ -191,21 +271,18 @@ class ProjectBatchProcessor {
|
|
|
191
271
|
(processed % 10 === 0 || processed === batch.size)) {
|
|
192
272
|
(0, logger_1.log)(this.wtag, `Progress: ${processed}/${batch.size} (${reindexed} reindexed)`);
|
|
193
273
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
metaDeletes.push(absPath);
|
|
197
|
-
reindexed++;
|
|
198
|
-
completed.add(absPath);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
// change or add
|
|
274
|
+
// Reclassify every event at apply time. A catchup-derived unlink may be
|
|
275
|
+
// stale if the file was recreated after its directory was scanned.
|
|
202
276
|
try {
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
277
|
+
const classification = yield this.filePolicy.classifyFile(absPath);
|
|
278
|
+
if (classification.status === "error") {
|
|
279
|
+
console.error(`[${this.wtag}] Policy could not classify ${absPath}:`, classification.error);
|
|
280
|
+
retryFailures.add(absPath);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (classification.status === "excluded" ||
|
|
284
|
+
classification.status === "missing") {
|
|
285
|
+
if (event === "unlink" || this.metaCache.get(absPath)) {
|
|
209
286
|
deletes.push(absPath);
|
|
210
287
|
metaDeletes.push(absPath);
|
|
211
288
|
reindexed++;
|
|
@@ -213,6 +290,7 @@ class ProjectBatchProcessor {
|
|
|
213
290
|
completed.add(absPath);
|
|
214
291
|
continue;
|
|
215
292
|
}
|
|
293
|
+
const stats = classification.stat;
|
|
216
294
|
const cached = this.metaCache.get(absPath);
|
|
217
295
|
if ((0, cache_check_1.isFileCached)(cached, stats)) {
|
|
218
296
|
completed.add(absPath);
|
|
@@ -221,10 +299,16 @@ class ProjectBatchProcessor {
|
|
|
221
299
|
// Fast path: if only mtime changed but size matches and we have a hash,
|
|
222
300
|
// verify in-process instead of dispatching to a worker (~220ms saved).
|
|
223
301
|
if ((cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
|
|
224
|
-
const
|
|
225
|
-
|
|
302
|
+
const snapshot = yield (0, file_utils_1.readFileSnapshot)(absPath, {
|
|
303
|
+
projectRoot: this.projectRoot,
|
|
304
|
+
});
|
|
305
|
+
if (snapshot.size !== stats.size ||
|
|
306
|
+
snapshot.mtimeMs !== stats.mtimeMs) {
|
|
307
|
+
throw new Error("File changed after policy classification");
|
|
308
|
+
}
|
|
309
|
+
const hash = (0, file_utils_1.computeContentHash)(snapshot.buffer, absPath);
|
|
226
310
|
if (hash === cached.hash) {
|
|
227
|
-
metaUpdates.set(absPath, Object.assign(Object.assign({}, cached), { mtimeMs:
|
|
311
|
+
metaUpdates.set(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: snapshot.mtimeMs }));
|
|
228
312
|
completed.add(absPath);
|
|
229
313
|
continue;
|
|
230
314
|
}
|
|
@@ -232,7 +316,27 @@ class ProjectBatchProcessor {
|
|
|
232
316
|
const result = yield pool.processFile({
|
|
233
317
|
path: absPath,
|
|
234
318
|
absolutePath: absPath,
|
|
319
|
+
projectRoot: this.projectRoot,
|
|
235
320
|
}, batchAc.signal);
|
|
321
|
+
// Policy and filesystem state can change while embedding is in
|
|
322
|
+
// flight. Never commit a result that no longer describes the path.
|
|
323
|
+
const current = yield this.filePolicy.classifyFile(absPath);
|
|
324
|
+
if (current.status === "error") {
|
|
325
|
+
retryFailures.add(absPath);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (current.status === "excluded" || current.status === "missing") {
|
|
329
|
+
deletes.push(absPath);
|
|
330
|
+
metaDeletes.push(absPath);
|
|
331
|
+
reindexed++;
|
|
332
|
+
completed.add(absPath);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (current.stat.mtimeMs !== result.mtimeMs ||
|
|
336
|
+
current.stat.size !== result.size) {
|
|
337
|
+
retryFailures.add(absPath);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
236
340
|
const metaEntry = {
|
|
237
341
|
hash: result.hash,
|
|
238
342
|
mtimeMs: result.mtimeMs,
|
|
@@ -274,15 +378,28 @@ class ProjectBatchProcessor {
|
|
|
274
378
|
console.error(`[${this.wtag}] Worker pool unhealthy, aborting batch`);
|
|
275
379
|
break;
|
|
276
380
|
}
|
|
277
|
-
|
|
381
|
+
retryFailures.add(absPath);
|
|
278
382
|
}
|
|
279
383
|
}
|
|
280
384
|
}
|
|
385
|
+
const pureDeleteCandidates = new Set(metaDeletes);
|
|
386
|
+
const authoritativeMetaDeletes = new Set();
|
|
387
|
+
for (const absPath of pureDeleteCandidates) {
|
|
388
|
+
const current = yield this.filePolicy.classifyFile(absPath);
|
|
389
|
+
if (current.status === "missing" || current.status === "excluded") {
|
|
390
|
+
authoritativeMetaDeletes.add(absPath);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
completed.delete(absPath);
|
|
394
|
+
batch.set(absPath, "change");
|
|
395
|
+
if (current.status === "error")
|
|
396
|
+
retryFailures.add(absPath);
|
|
397
|
+
}
|
|
281
398
|
// Requeue files that didn't reach a terminal outcome. This includes the
|
|
282
399
|
// file whose worker was in flight when the batch was aborted.
|
|
283
400
|
for (const [absPath, event] of batch) {
|
|
284
|
-
if (!completed.has(absPath)
|
|
285
|
-
|
|
401
|
+
if (!completed.has(absPath)) {
|
|
402
|
+
requeuePath(absPath, event, retryFailures.has(absPath));
|
|
286
403
|
}
|
|
287
404
|
}
|
|
288
405
|
// Flush to VectorDB: insert first, then delete old (preserving new)
|
|
@@ -290,29 +407,44 @@ class ProjectBatchProcessor {
|
|
|
290
407
|
if (vectors.length > 0) {
|
|
291
408
|
yield this.vectorDb.insertBatch(vectors);
|
|
292
409
|
}
|
|
293
|
-
|
|
410
|
+
for (const absPath of [...authoritativeMetaDeletes]) {
|
|
411
|
+
const current = yield this.filePolicy.classifyFile(absPath);
|
|
412
|
+
if (current.status === "missing" || current.status === "excluded") {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
authoritativeMetaDeletes.delete(absPath);
|
|
416
|
+
completed.delete(absPath);
|
|
417
|
+
batch.set(absPath, "change");
|
|
418
|
+
requeuePath(absPath, "change", current.status === "error");
|
|
419
|
+
}
|
|
420
|
+
const deletesToApply = deletes.filter((absPath) => !pureDeleteCandidates.has(absPath) ||
|
|
421
|
+
authoritativeMetaDeletes.has(absPath));
|
|
422
|
+
if (deletesToApply.length > 0) {
|
|
294
423
|
if (newIds.length > 0) {
|
|
295
|
-
yield this.vectorDb.deletePathsExcludingIds(
|
|
424
|
+
yield this.vectorDb.deletePathsExcludingIds(deletesToApply, newIds);
|
|
296
425
|
}
|
|
297
426
|
else {
|
|
298
|
-
yield this.vectorDb.deletePaths(
|
|
427
|
+
yield this.vectorDb.deletePaths(deletesToApply);
|
|
299
428
|
}
|
|
300
429
|
}
|
|
301
430
|
// Update MetaCache
|
|
302
431
|
for (const [p, entry] of metaUpdates) {
|
|
303
432
|
this.metaCache.put(p, entry);
|
|
304
433
|
}
|
|
305
|
-
for (const p of
|
|
434
|
+
for (const p of authoritativeMetaDeletes) {
|
|
306
435
|
this.metaCache.delete(p);
|
|
307
436
|
}
|
|
308
437
|
const duration = Date.now() - start;
|
|
309
438
|
if (reindexed > 0) {
|
|
310
|
-
(
|
|
439
|
+
(_b = this.onReindex) === null || _b === void 0 ? void 0 : _b.call(this, reindexed, duration);
|
|
311
440
|
}
|
|
312
441
|
const remaining = this.pending.size;
|
|
313
442
|
(0, logger_1.log)(this.wtag, `Batch complete: ${batch.size} files, ${reindexed} reindexed (${(duration / 1000).toFixed(1)}s)${remaining > 0 ? ` — ${remaining} remaining` : ""}`);
|
|
314
|
-
for (const absPath of
|
|
443
|
+
for (const absPath of completed) {
|
|
315
444
|
this.retryCount.delete(absPath);
|
|
445
|
+
this.retryAt.delete(absPath);
|
|
446
|
+
this.terminalFailures.delete(absPath);
|
|
447
|
+
(_c = this.onPathSuccess) === null || _c === void 0 ? void 0 : _c.call(this, absPath);
|
|
316
448
|
}
|
|
317
449
|
// Trigger compaction if fragments are accumulating
|
|
318
450
|
if (reindexed > 0) {
|
|
@@ -328,6 +460,8 @@ class ProjectBatchProcessor {
|
|
|
328
460
|
// Disk pressure: requeue without counting as retries (not the file's fault)
|
|
329
461
|
if (err instanceof vector_db_1.DiskPressureError) {
|
|
330
462
|
for (const [absPath, event] of batch) {
|
|
463
|
+
if (this.terminalFailures.has(absPath))
|
|
464
|
+
continue;
|
|
331
465
|
if (!this.pending.has(absPath)) {
|
|
332
466
|
this.pending.set(absPath, event);
|
|
333
467
|
}
|
|
@@ -338,51 +472,81 @@ class ProjectBatchProcessor {
|
|
|
338
472
|
}
|
|
339
473
|
else if ((0, vector_db_1.isLanceCorruptionError)(err)) {
|
|
340
474
|
// Manifest references a missing fragment — retrying every 2s burns CPU
|
|
341
|
-
// and floods logs without making progress. Log once per hour,
|
|
342
|
-
//
|
|
343
|
-
// human can run `gmax index --reset` for the affected project.
|
|
475
|
+
// and floods logs without making progress. Log once per hour, preserve
|
|
476
|
+
// non-terminal events, and back off 30 min for manual recovery.
|
|
344
477
|
const now = Date.now();
|
|
345
478
|
if (now - this.lastCorruptionLogMs > 60 * 60 * 1000) {
|
|
346
479
|
this.lastCorruptionLogMs = now;
|
|
347
480
|
const msg = err instanceof Error ? err.message : String(err);
|
|
348
481
|
console.error(`[${this.wtag}] DATA CORRUPTION: LanceDB manifest references a missing fragment. ` +
|
|
349
482
|
`Backing off this project's batch processor for 30 min. ` +
|
|
350
|
-
`
|
|
483
|
+
`Preserve the store for diagnosis, then run 'gmax repair --rebuild' only when whole-corpus replacement is intended. Original: ${msg}`);
|
|
484
|
+
}
|
|
485
|
+
for (const [absPath, event] of batch) {
|
|
486
|
+
if (this.terminalFailures.has(absPath))
|
|
487
|
+
continue;
|
|
488
|
+
if (!this.pending.has(absPath))
|
|
489
|
+
this.pending.set(absPath, event);
|
|
351
490
|
}
|
|
352
|
-
for (const [absPath] of batch)
|
|
353
|
-
this.retryCount.delete(absPath);
|
|
354
491
|
backoffOverrideMs = 30 * 60 * 1000;
|
|
355
492
|
}
|
|
356
493
|
else {
|
|
357
494
|
console.error(`[${this.wtag}] Batch processing failed:`, err);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
495
|
+
let dropped = 0;
|
|
496
|
+
const droppedPaths = [];
|
|
497
|
+
for (const [absPath, event] of batch) {
|
|
498
|
+
if (this.terminalFailures.has(absPath))
|
|
499
|
+
continue;
|
|
500
|
+
if (this.pending.has(absPath))
|
|
501
|
+
continue;
|
|
502
|
+
const retry = (0, watcher_batch_1.computePathRetry)((_d = this.retryCount.get(absPath)) !== null && _d !== void 0 ? _d : 0, MAX_RETRIES, DEBOUNCE_MS);
|
|
503
|
+
if (!retry.retry) {
|
|
504
|
+
this.retryCount.delete(absPath);
|
|
505
|
+
this.retryAt.delete(absPath);
|
|
506
|
+
this.terminalFailures.add(absPath);
|
|
507
|
+
(_e = this.onTerminalFailure) === null || _e === void 0 ? void 0 : _e.call(this, absPath);
|
|
508
|
+
dropped++;
|
|
509
|
+
droppedPaths.push(absPath);
|
|
510
|
+
continue;
|
|
362
511
|
}
|
|
512
|
+
this.retryCount.set(absPath, retry.failures);
|
|
513
|
+
this.retryAt.set(absPath, Date.now() + retry.backoffMs);
|
|
514
|
+
this.pending.set(absPath, event);
|
|
363
515
|
}
|
|
364
516
|
if (dropped > 0) {
|
|
365
|
-
const droppedPaths = [...batch.keys()].filter((p) => !requeued.has(p));
|
|
366
517
|
(0, logger_1.log)(this.wtag, `Dropped ${dropped} file(s) after ${MAX_RETRIES} retries: ${droppedPaths.map((p) => path.basename(p)).join(", ")}`);
|
|
367
518
|
}
|
|
368
|
-
backoffOverrideMs =
|
|
519
|
+
backoffOverrideMs = 0;
|
|
369
520
|
}
|
|
370
521
|
}
|
|
371
522
|
finally {
|
|
372
523
|
clearTimeout(batchTimeout);
|
|
524
|
+
operationSignal.removeEventListener("abort", abortBatch);
|
|
373
525
|
this.currentBatchAc = null;
|
|
374
526
|
this.processing = false;
|
|
527
|
+
if (this.policyChangedDuringBatch) {
|
|
528
|
+
this.policyChangedDuringBatch = false;
|
|
529
|
+
for (const absPath of batch.keys()) {
|
|
530
|
+
if (!this.pending.has(absPath)) {
|
|
531
|
+
this.retryCount.delete(absPath);
|
|
532
|
+
this.retryAt.delete(absPath);
|
|
533
|
+
this.pending.set(absPath, "change");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
375
537
|
if (!this.closed && this.pending.size > 0) {
|
|
376
538
|
if (backoffOverrideMs > 0) {
|
|
377
539
|
if (this.debounceTimer)
|
|
378
540
|
clearTimeout(this.debounceTimer);
|
|
541
|
+
this.debounceDueMs = Date.now() + backoffOverrideMs;
|
|
379
542
|
this.debounceTimer = setTimeout(() => {
|
|
380
543
|
this.debounceTimer = null;
|
|
544
|
+
this.debounceDueMs = 0;
|
|
381
545
|
this.startBatch();
|
|
382
546
|
}, backoffOverrideMs);
|
|
383
547
|
}
|
|
384
548
|
else {
|
|
385
|
-
this.
|
|
549
|
+
this.schedulePendingBatch();
|
|
386
550
|
}
|
|
387
551
|
}
|
|
388
552
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeEmbeddingFingerprint = computeEmbeddingFingerprint;
|
|
4
|
+
exports.resolveEmbeddingGeneration = resolveEmbeddingGeneration;
|
|
5
|
+
exports.parseEmbeddingGeneration = parseEmbeddingGeneration;
|
|
6
|
+
exports.isOnnxFallbackCompatible = isOnnxFallbackCompatible;
|
|
7
|
+
exports.compareEmbeddingGeneration = compareEmbeddingGeneration;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const config_1 = require("../../config");
|
|
10
|
+
function requireModelId(label, value) {
|
|
11
|
+
const normalized = value.trim();
|
|
12
|
+
if (!normalized)
|
|
13
|
+
throw new Error(`${label} must be a non-empty model ID`);
|
|
14
|
+
if (normalized !== value) {
|
|
15
|
+
throw new Error(`${label} must not contain surrounding whitespace`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function computeEmbeddingFingerprint(identity) {
|
|
20
|
+
if (!Number.isInteger(identity.vectorDim) || identity.vectorDim <= 0) {
|
|
21
|
+
throw new Error("Embedding vector dimension must be a positive integer");
|
|
22
|
+
}
|
|
23
|
+
const canonical = [
|
|
24
|
+
"gmax-embedding-generation-v1",
|
|
25
|
+
requireModelId("Model tier", identity.tier),
|
|
26
|
+
identity.vectorDim,
|
|
27
|
+
requireModelId("ONNX model", identity.onnxModel),
|
|
28
|
+
requireModelId("MLX model", identity.mlxModel),
|
|
29
|
+
requireModelId("ColBERT model", identity.colbertModel),
|
|
30
|
+
];
|
|
31
|
+
return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function resolveEmbeddingGeneration(input) {
|
|
34
|
+
var _a;
|
|
35
|
+
const tier = config_1.MODEL_TIERS[input.modelTier];
|
|
36
|
+
if (!tier)
|
|
37
|
+
throw new Error(`Unknown model tier: ${input.modelTier}`);
|
|
38
|
+
if (input.vectorDim !== undefined && input.vectorDim !== tier.vectorDim) {
|
|
39
|
+
throw new Error(`Vector dimension ${input.vectorDim} contradicts model tier ${input.modelTier} (${tier.vectorDim})`);
|
|
40
|
+
}
|
|
41
|
+
const identity = {
|
|
42
|
+
tier: tier.id,
|
|
43
|
+
vectorDim: tier.vectorDim,
|
|
44
|
+
onnxModel: tier.onnxModel,
|
|
45
|
+
mlxModel: requireModelId("MLX model", (_a = input.mlxModel) !== null && _a !== void 0 ? _a : tier.mlxModel),
|
|
46
|
+
colbertModel: config_1.MODEL_IDS.colbert,
|
|
47
|
+
};
|
|
48
|
+
return Object.freeze(Object.assign(Object.assign({}, identity), { fingerprint: computeEmbeddingFingerprint(identity) }));
|
|
49
|
+
}
|
|
50
|
+
function parseEmbeddingGeneration(value) {
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
52
|
+
throw new Error("Invalid embedding generation");
|
|
53
|
+
}
|
|
54
|
+
const candidate = value;
|
|
55
|
+
if (typeof candidate.tier !== "string" ||
|
|
56
|
+
typeof candidate.vectorDim !== "number" ||
|
|
57
|
+
typeof candidate.onnxModel !== "string" ||
|
|
58
|
+
typeof candidate.mlxModel !== "string" ||
|
|
59
|
+
typeof candidate.colbertModel !== "string" ||
|
|
60
|
+
typeof candidate.fingerprint !== "string") {
|
|
61
|
+
throw new Error("Invalid embedding generation");
|
|
62
|
+
}
|
|
63
|
+
const identity = {
|
|
64
|
+
tier: candidate.tier,
|
|
65
|
+
vectorDim: candidate.vectorDim,
|
|
66
|
+
onnxModel: candidate.onnxModel,
|
|
67
|
+
mlxModel: candidate.mlxModel,
|
|
68
|
+
colbertModel: candidate.colbertModel,
|
|
69
|
+
};
|
|
70
|
+
if (candidate.fingerprint !== computeEmbeddingFingerprint(identity)) {
|
|
71
|
+
throw new Error("Embedding generation identity is inconsistent");
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze(Object.assign(Object.assign({}, identity), { fingerprint: candidate.fingerprint }));
|
|
74
|
+
}
|
|
75
|
+
function isOnnxFallbackCompatible(generation) {
|
|
76
|
+
const tier = config_1.MODEL_TIERS[generation.tier];
|
|
77
|
+
return ((tier === null || tier === void 0 ? void 0 : tier.id) === generation.tier &&
|
|
78
|
+
tier.vectorDim === generation.vectorDim &&
|
|
79
|
+
tier.onnxModel === generation.onnxModel &&
|
|
80
|
+
tier.mlxModel === generation.mlxModel);
|
|
81
|
+
}
|
|
82
|
+
function compareEmbeddingGeneration(persisted, configured) {
|
|
83
|
+
var _a;
|
|
84
|
+
const hasExactIdentity = persisted.embeddingFingerprint !== undefined &&
|
|
85
|
+
persisted.embedModel !== undefined &&
|
|
86
|
+
persisted.mlxModel !== undefined &&
|
|
87
|
+
persisted.colbertModel !== undefined;
|
|
88
|
+
const built = hasExactIdentity
|
|
89
|
+
? parseEmbeddingGeneration({
|
|
90
|
+
tier: persisted.modelTier,
|
|
91
|
+
vectorDim: persisted.vectorDim,
|
|
92
|
+
onnxModel: persisted.embedModel,
|
|
93
|
+
mlxModel: persisted.mlxModel,
|
|
94
|
+
colbertModel: persisted.colbertModel,
|
|
95
|
+
fingerprint: persisted.embeddingFingerprint,
|
|
96
|
+
})
|
|
97
|
+
: resolveEmbeddingGeneration(Object.assign({ modelTier: persisted.modelTier, vectorDim: persisted.vectorDim }, (persisted.mlxModel === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { mlxModel: persisted.mlxModel })));
|
|
100
|
+
return {
|
|
101
|
+
built,
|
|
102
|
+
state: ((_a = persisted.embeddingFingerprint) !== null && _a !== void 0 ? _a : built.fingerprint) !==
|
|
103
|
+
configured.fingerprint
|
|
104
|
+
? "stale"
|
|
105
|
+
: persisted.embeddingFingerprint
|
|
106
|
+
? "current"
|
|
107
|
+
: "legacy",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectEmbeddingStatus = projectEmbeddingStatus;
|
|
4
|
+
exports.embeddingFingerprintLabel = embeddingFingerprintLabel;
|
|
5
|
+
exports.assertEmbeddingSearchCompatible = assertEmbeddingSearchCompatible;
|
|
6
|
+
const embedding_generation_1 = require("./embedding-generation");
|
|
7
|
+
function projectEmbeddingStatus(project, config) {
|
|
8
|
+
const configured = (0, embedding_generation_1.resolveEmbeddingGeneration)(config);
|
|
9
|
+
if (!project || project.status !== "indexed") {
|
|
10
|
+
return { configured, built: null, state: "unbuilt" };
|
|
11
|
+
}
|
|
12
|
+
const comparison = (0, embedding_generation_1.compareEmbeddingGeneration)(project, configured);
|
|
13
|
+
return Object.assign({ configured }, comparison);
|
|
14
|
+
}
|
|
15
|
+
function embeddingFingerprintLabel(fingerprint) {
|
|
16
|
+
return fingerprint.slice(0, 12);
|
|
17
|
+
}
|
|
18
|
+
function assertEmbeddingSearchCompatible(projects, config) {
|
|
19
|
+
const stale = projects.filter((project) => projectEmbeddingStatus(project, config).state === "stale");
|
|
20
|
+
if (stale.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
throw new Error(`embedding generation mismatch for ${stale.map((project) => project.name).join(", ")}; run gmax repair --rebuild to rebuild the whole corpus`);
|
|
23
|
+
}
|