grepmax 0.26.62 → 0.26.64
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 +7 -1
- package/dist/commands/mcp.js +8 -5
- package/dist/commands/status.js +14 -10
- package/dist/commands/watch.js +5 -2
- package/dist/lib/daemon/daemon.js +11 -8
- package/dist/lib/daemon/watcher-manager.js +78 -30
- package/dist/lib/index/batch-processor.js +117 -21
- package/dist/lib/index/watcher-errors.js +16 -0
- package/dist/lib/index/watcher.js +45 -3
- package/dist/lib/output/index-state-footer.js +7 -0
- package/dist/lib/output/mcp-results.js +9 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "grepmax",
|
|
11
11
|
"source": "./plugins/grepmax",
|
|
12
12
|
"description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
|
|
13
|
-
"version": "0.26.
|
|
13
|
+
"version": "0.26.64",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
package/README.md
CHANGED
|
@@ -227,6 +227,8 @@ The CLI and MCP output include score, max similarity, pair count, representative
|
|
|
227
227
|
|
|
228
228
|
A single daemon watches your projects via native OS file events (FSEvents/inotify). Changes are detected in sub-second and incrementally reindexed. All writes to LanceDB are routed through the daemon via IPC, eliminating lock contention.
|
|
229
229
|
|
|
230
|
+
Filesystem watching belongs to gmax, in both daemon and standalone watch modes. Hetchy Developer is an optional monitor/controller that reads gmax status and invokes gmax commands. Watching, indexing and search must work with the dev app and its helper absent or stopped. Any future shared watcher extraction must be an independent component or package with an interface and lifecycle usable without Hetchy Developer; the dev app cannot become a gmax runtime dependency.
|
|
231
|
+
|
|
230
232
|
The daemon watches only projects that are in use: each Claude Code session (its MCP server and session hooks) holds a lease on its project, and `gmax add` / `gmax index` take a short one. When the last lease ends the project is unwatched; its index stays searchable, and the next session's catch-up scan picks up whatever changed in between. Set `GMAX_WATCH_ALL=1` to watch every registered project instead.
|
|
231
233
|
|
|
232
234
|
```bash
|
|
@@ -238,7 +240,11 @@ gmax status # See all projects + watcher status
|
|
|
238
240
|
|
|
239
241
|
Adding or indexing a project can start the daemon automatically. MCP acquires primary-project watch leases lazily when an index-reading tool is used; discovery and catalog listing start no background work. It shuts down after 4 hours of inactivity, and hands off to a fresh daemon after 24 hours or once its memory footprint passes 2.5 GB. File-change batches are processed on up to four worker processes by default, added only when work backs up, with one kept free for searches; full-table compaction and new FTS/ANN index builds are disabled by containment. Existing indexes remain readable. Host safety stops persist across restart and block heavy operations.
|
|
240
242
|
|
|
241
|
-
`gmax watch status` reports failed files, native watcher recovery or polling, dropped-event counts, and the last complete filesystem reconciliation.
|
|
243
|
+
`gmax watch status` reports failed files, native watcher recovery or polling, dropped-event counts, and the last complete filesystem reconciliation. FSEvents dropped-event warnings keep the native stream attached and preserve the accompanying events. A reconciliation covers the gap; repeated warnings coalesce into one later scan, with a 30-second scan interval. Recovery stays visible until a complete scan covers the latest gap. Other backend errors still trigger subscription recovery; repeated terminal failures fall back to five-minute polling, where recent edits can lag. Search and MCP responses retain health warnings even after the file queue drains. A complete reconciliation means the filesystem scan completed; queued embedding work can still be pending.
|
|
244
|
+
|
|
245
|
+
Live filesystem edits take priority over background catchup and cleanup. Background batches dispatch one file at a time, with at most four catchup files or 50 cleanup paths. They yield between files when a live edit arrives or the two-second dispatch window is reached; an active file or store commit finishes before yielding. Catchup and cleanup alternate, and one background file is admitted after three consecutive live batches so cleanup continues during sustained editing. Existing retry backoff and worker limits remain in effect.
|
|
246
|
+
|
|
247
|
+
`gmax status --json` exposes `projects[].health.queue`; daemon IPC and MCP `index_status` expose the same `indexState.queue`. Counts `live`, `catchup` and `cleanup` cover waiting paths; `activeFiles` covers the selected current batch, including files not yet dispatched. `oldestLiveEditAgeMs` includes both waiting and current-batch live edits until commit, and is null when none remain. Human/agent status, `gmax watch status` and MCP health text show these measures separately. Initial full indexing uses its existing progress counters rather than these watcher queues. Older daemons may omit queue diagnostics.
|
|
242
248
|
|
|
243
249
|
Temporary embedding backend outages preserve existing indexed rows and retry with backoff from five seconds up to one minute, without exhausting individual files' retry budgets. MLX failures report bounded transport/protocol details in the daemon log. `gmax status --json` includes the most recent resource snapshot when a recycle threshold has been reached; these counters help investigate memory growth, whose cause requires measurement over time.
|
|
244
250
|
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1842,7 +1842,8 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1842
1842
|
}
|
|
1843
1843
|
function handleIndexStatus() {
|
|
1844
1844
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1845
|
-
var _a
|
|
1845
|
+
var _a;
|
|
1846
|
+
var _b, _c, _d, _e, _f;
|
|
1846
1847
|
try {
|
|
1847
1848
|
const globalConfig = storeConfig();
|
|
1848
1849
|
const projects = storeProjects();
|
|
@@ -1860,7 +1861,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1860
1861
|
: (0, watcher_store_1.getWatcherCoveringPath)(currentRoot());
|
|
1861
1862
|
let watcherLine = "Watcher: not running";
|
|
1862
1863
|
if (watcher) {
|
|
1863
|
-
const status = (
|
|
1864
|
+
const status = (_b = watcher.status) !== null && _b !== void 0 ? _b : "unknown";
|
|
1864
1865
|
const root = path.basename(watcher.projectRoot);
|
|
1865
1866
|
const reindex = watcher.lastReindex
|
|
1866
1867
|
? `last reindex: ${Math.round((Date.now() - watcher.lastReindex) / 60000)}m ago`
|
|
@@ -1871,12 +1872,14 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1871
1872
|
}
|
|
1872
1873
|
}
|
|
1873
1874
|
if (daemonProject) {
|
|
1874
|
-
watcherLine = `Watcher: ${(
|
|
1875
|
+
watcherLine = `Watcher: ${(_c = daemonProject.status) !== null && _c !== void 0 ? _c : "unknown"} (${path.basename(daemonProject.root)}/)`;
|
|
1875
1876
|
const health = (0, index_state_footer_1.formatIndexStateFooter)(daemonProject.indexState, {
|
|
1876
1877
|
agent: true,
|
|
1877
1878
|
});
|
|
1878
1879
|
if (health)
|
|
1879
1880
|
watcherLine += `\n${health}`;
|
|
1881
|
+
if ((_a = daemonProject.indexState) === null || _a === void 0 ? void 0 : _a.queue)
|
|
1882
|
+
watcherLine += `\nQueue: ${(0, index_state_footer_1.formatWatchQueue)(daemonProject.indexState.queue)}`;
|
|
1880
1883
|
}
|
|
1881
1884
|
// Health overview only. The per-project listing (names, roots,
|
|
1882
1885
|
// per-project chunk counts) lives in `list_projects` — keep this tool
|
|
@@ -1919,8 +1922,8 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1919
1922
|
: null,
|
|
1920
1923
|
},
|
|
1921
1924
|
watcher: {
|
|
1922
|
-
status: (
|
|
1923
|
-
indexState: (
|
|
1925
|
+
status: (_e = (_d = daemonProject === null || daemonProject === void 0 ? void 0 : daemonProject.status) !== null && _d !== void 0 ? _d : watcher === null || watcher === void 0 ? void 0 : watcher.status) !== null && _e !== void 0 ? _e : (secondary() || !daemonStatus.ok ? "unobserved" : "not_running"),
|
|
1926
|
+
indexState: (_f = daemonProject === null || daemonProject === void 0 ? void 0 : daemonProject.indexState) !== null && _f !== void 0 ? _f : null,
|
|
1924
1927
|
},
|
|
1925
1928
|
compaction: compaction.success ? compaction.data : null,
|
|
1926
1929
|
});
|
package/dist/commands/status.js
CHANGED
|
@@ -273,8 +273,8 @@ Examples:
|
|
|
273
273
|
gmax status --json The same, plus daemon and settings, as JSON
|
|
274
274
|
`)
|
|
275
275
|
.action((opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
276
|
-
var _a, _b, _c, _d;
|
|
277
|
-
var
|
|
276
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
277
|
+
var _j, _k, _l, _m, _o, _p, _q, _r;
|
|
278
278
|
const globalConfig = (0, index_config_1.readGlobalConfig)();
|
|
279
279
|
const projects = (0, project_registry_1.listProjects)();
|
|
280
280
|
const indexing = (0, lock_1.isLocked)(config_1.PATHS.globalRoot);
|
|
@@ -316,7 +316,7 @@ Examples:
|
|
|
316
316
|
console.log(`\n${style.bold("gmax")} · ${globalConfig.modelTier} (${globalConfig.vectorDim}d, ${globalConfig.embedMode})${indexing ? style.yellow(" · indexing...") : ""}`);
|
|
317
317
|
}
|
|
318
318
|
if (((_b = (_a = view.daemon) === null || _a === void 0 ? void 0 : _a.service) === null || _b === void 0 ? void 0 : _b.mode) === "paused") {
|
|
319
|
-
const reason = ((
|
|
319
|
+
const reason = ((_j = view.daemon.service.reason) !== null && _j !== void 0 ? _j : "host pressure").replace(/[\r\n\t]/g, " ");
|
|
320
320
|
console.log(opts.agent
|
|
321
321
|
? `daemon_service\tmode=paused\treason=${reason}`
|
|
322
322
|
: style.yellow(`Bounded reads available; indexing and embeddings paused: ${reason}`));
|
|
@@ -331,14 +331,14 @@ Examples:
|
|
|
331
331
|
if (opts.agent) {
|
|
332
332
|
console.log(`store\t${store.home}\t${store.state}\tprefixes=${store.prefixes.join(",")}${store.error ? `\terror=${store.error}` : ""}`);
|
|
333
333
|
for (const project of store.projects)
|
|
334
|
-
console.log(`store_project\t${store.home}\t${project.name}\t${(
|
|
334
|
+
console.log(`store_project\t${store.home}\t${project.name}\t${(_k = project.chunkCount) !== null && _k !== void 0 ? _k : 0}\t${(_l = project.status) !== null && _l !== void 0 ? _l : "indexed"}\tcounts=cached`);
|
|
335
335
|
}
|
|
336
336
|
else {
|
|
337
337
|
console.log(`\nStore: ${shortenPath(store.home)} — ${store.state}${store.error ? ` (${store.error})` : ""}`);
|
|
338
338
|
if (store.state === "offline")
|
|
339
339
|
console.log(" Connect the drive to list its projects.");
|
|
340
340
|
for (const project of store.projects)
|
|
341
|
-
console.log(` ${project.name} ${formatChunks(project.chunkCount)} chunks (cached) ${(
|
|
341
|
+
console.log(` ${project.name} ${formatChunks(project.chunkCount)} chunks (cached) ${(_m = project.status) !== null && _m !== void 0 ? _m : "indexed"}`);
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
if (projects.length === 0) {
|
|
@@ -355,9 +355,10 @@ Examples:
|
|
|
355
355
|
for (const project of projects) {
|
|
356
356
|
const st = projectState(project, watchers.get(project.root));
|
|
357
357
|
const isCurrent = project.root === currentRoot;
|
|
358
|
-
const count = (
|
|
358
|
+
const count = (_o = chunkCounts.get(project.root)) !== null && _o !== void 0 ? _o : project.chunkCount;
|
|
359
359
|
const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
|
|
360
|
-
|
|
360
|
+
const queue = (_d = (_c = view.health) === null || _c === void 0 ? void 0 : _c.get(project.root)) === null || _d === void 0 ? void 0 : _d.queue;
|
|
361
|
+
console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}${((_e = view.health) === null || _e === void 0 ? void 0 : _e.get(project.root)) ? `\t${(_p = (0, index_state_footer_1.formatIndexStateFooter)(view.health.get(project.root), { agent: true })) !== null && _p !== void 0 ? _p : ""}` : ""}${queue ? `\t${(0, index_state_footer_1.formatWatchQueue)(queue)}` : ""}`);
|
|
361
362
|
}
|
|
362
363
|
const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)((0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig), { agent: true });
|
|
363
364
|
if (legacyNotice)
|
|
@@ -373,7 +374,7 @@ Examples:
|
|
|
373
374
|
const watcher = watchers.get(project.root);
|
|
374
375
|
// Status column
|
|
375
376
|
let statusStr;
|
|
376
|
-
const projectStatus = (
|
|
377
|
+
const projectStatus = (_q = project.status) !== null && _q !== void 0 ? _q : "indexed";
|
|
377
378
|
if (projectStatus === "pending") {
|
|
378
379
|
statusStr = style.yellow("pending");
|
|
379
380
|
}
|
|
@@ -393,7 +394,7 @@ Examples:
|
|
|
393
394
|
statusStr = style.dim("idle");
|
|
394
395
|
}
|
|
395
396
|
// Chunks column
|
|
396
|
-
const count = (
|
|
397
|
+
const count = (_r = chunkCounts.get(project.root)) !== null && _r !== void 0 ? _r : project.chunkCount;
|
|
397
398
|
const chunks = `${formatChunks(count)} chunks`;
|
|
398
399
|
// Age column
|
|
399
400
|
const age = formatAge(project.lastIndexed);
|
|
@@ -402,11 +403,14 @@ Examples:
|
|
|
402
403
|
const marker = isCurrent ? style.cyan(" ←") : "";
|
|
403
404
|
const name = project.name.padEnd(nameWidth);
|
|
404
405
|
console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr} embedding:${embedding}${marker}`);
|
|
405
|
-
const health = (0, index_state_footer_1.formatIndexStateFooter)((
|
|
406
|
+
const health = (0, index_state_footer_1.formatIndexStateFooter)((_f = view.health) === null || _f === void 0 ? void 0 : _f.get(project.root), {
|
|
406
407
|
agent: false,
|
|
407
408
|
});
|
|
408
409
|
if (health)
|
|
409
410
|
console.log(` ${health}`);
|
|
411
|
+
const queue = (_h = (_g = view.health) === null || _g === void 0 ? void 0 : _g.get(project.root)) === null || _h === void 0 ? void 0 : _h.queue;
|
|
412
|
+
if (queue)
|
|
413
|
+
console.log(` queue: ${(0, index_state_footer_1.formatWatchQueue)(queue)}`);
|
|
410
414
|
}
|
|
411
415
|
const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)((0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig));
|
|
412
416
|
if (legacyNotice)
|
package/dist/commands/watch.js
CHANGED
|
@@ -401,7 +401,8 @@ exports.watch
|
|
|
401
401
|
.command("status")
|
|
402
402
|
.description("Show running watchers")
|
|
403
403
|
.action(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
404
|
-
var _a
|
|
404
|
+
var _a;
|
|
405
|
+
var _b, _c, _d;
|
|
405
406
|
const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
406
407
|
const resp = yield sendDaemonCommand({ cmd: "status" });
|
|
407
408
|
if (resp.ok) {
|
|
@@ -417,8 +418,10 @@ exports.watch
|
|
|
417
418
|
const health = (0, index_state_footer_1.formatIndexStateFooter)(p.indexState, { agent: true });
|
|
418
419
|
if (health)
|
|
419
420
|
console.log(` ${health}`);
|
|
421
|
+
if ((_a = p.indexState) === null || _a === void 0 ? void 0 : _a.queue)
|
|
422
|
+
console.log(` queue: ${(0, index_state_footer_1.formatWatchQueue)(p.indexState.queue)}`);
|
|
420
423
|
if (p.indexState)
|
|
421
|
-
console.log(` watcher=${(
|
|
424
|
+
console.log(` watcher=${(_b = p.indexState.watcherMode) !== null && _b !== void 0 ? _b : "unknown"} overflows=${(_c = p.indexState.overflowCount) !== null && _c !== void 0 ? _c : 0} lastReconciled=${p.indexState.lastReconciledAt ? new Date(p.indexState.lastReconciledAt).toISOString() : "unknown"} catchupMs=${(_d = p.indexState.catchupMs) !== null && _d !== void 0 ? _d : "unknown"}`);
|
|
422
425
|
}
|
|
423
426
|
if (projects.length === 0) {
|
|
424
427
|
console.log(" (no projects)");
|
|
@@ -980,13 +980,14 @@ class Daemon {
|
|
|
980
980
|
* on every search to annotate partial-result responses (Phase 6).
|
|
981
981
|
*/
|
|
982
982
|
indexState(root) {
|
|
983
|
-
var _a
|
|
984
|
-
var
|
|
983
|
+
var _a;
|
|
984
|
+
var _b, _c, _d, _e;
|
|
985
985
|
const processor = this.processors.get(root);
|
|
986
|
-
const
|
|
987
|
-
const
|
|
986
|
+
const progress = processor === null || processor === void 0 ? void 0 : processor.progress;
|
|
987
|
+
const batchPending = (_b = progress === null || progress === void 0 ? void 0 : progress.pendingFiles) !== null && _b !== void 0 ? _b : 0;
|
|
988
|
+
const processing = (_c = progress === null || progress === void 0 ? void 0 : progress.processing) !== null && _c !== void 0 ? _c : false;
|
|
988
989
|
// status === "pending" means the initial full index hasn't completed.
|
|
989
|
-
const initialPending = ((
|
|
990
|
+
const initialPending = ((_a = (0, project_registry_1.getProject)(root)) === null || _a === void 0 ? void 0 : _a.status) === "pending";
|
|
990
991
|
// A full index (--reset / initial) bypasses the batch processor; its
|
|
991
992
|
// onProgress feeds indexProgress, giving a real remaining count.
|
|
992
993
|
const fullIdx = this.indexProgress.get(root);
|
|
@@ -998,7 +999,7 @@ class Daemon {
|
|
|
998
999
|
// index genuinely has work outstanding; a post-FSEvents-drop catchup mostly
|
|
999
1000
|
// re-verifies unchanged files. Only claim "verifying" with a real sample and
|
|
1000
1001
|
// no initial-index work pending.
|
|
1001
|
-
const { recentFiles, recentReindexed } =
|
|
1002
|
+
const { recentFiles, recentReindexed } = progress !== null && progress !== void 0 ? progress : {
|
|
1002
1003
|
recentFiles: 0,
|
|
1003
1004
|
recentReindexed: 0,
|
|
1004
1005
|
};
|
|
@@ -1008,8 +1009,10 @@ class Daemon {
|
|
|
1008
1009
|
recentReindexed === 0 &&
|
|
1009
1010
|
pendingFiles > 0;
|
|
1010
1011
|
const health = this.watcherManager.health(root);
|
|
1011
|
-
return Object.assign(Object.assign({ indexing: this.pausedReason == null &&
|
|
1012
|
-
(!!fullIdx || processing || batchPending > 0 || initialPending), pendingFiles: this.pausedReason == null ? pendingFiles : 0, verifying: this.pausedReason == null && verifying }, health),
|
|
1012
|
+
return Object.assign(Object.assign(Object.assign({ indexing: this.pausedReason == null &&
|
|
1013
|
+
(!!fullIdx || processing || batchPending > 0 || initialPending), pendingFiles: this.pausedReason == null ? pendingFiles : 0, verifying: this.pausedReason == null && verifying }, health), (this.pausedReason == null && (progress === null || progress === void 0 ? void 0 : progress.queue)
|
|
1014
|
+
? { queue: progress.queue }
|
|
1015
|
+
: {})), { failedFiles: Math.max((_d = progress === null || progress === void 0 ? void 0 : progress.failedFiles) !== null && _d !== void 0 ? _d : 0, (_e = health.failedFiles) !== null && _e !== void 0 ? _e : 0) });
|
|
1013
1016
|
}
|
|
1014
1017
|
search(payload, signal) {
|
|
1015
1018
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -57,6 +57,7 @@ const cache_coherence_1 = require("../index/cache-coherence");
|
|
|
57
57
|
const file_policy_1 = require("../index/file-policy");
|
|
58
58
|
const walker_1 = require("../index/walker");
|
|
59
59
|
const watcher_1 = require("../index/watcher");
|
|
60
|
+
const watcher_errors_1 = require("../index/watcher-errors");
|
|
60
61
|
const watcher_ignore_1 = require("../index/watcher-ignore");
|
|
61
62
|
const file_utils_1 = require("../utils/file-utils");
|
|
62
63
|
const logger_1 = require("../utils/logger");
|
|
@@ -68,7 +69,7 @@ const watcher_store_1 = require("../utils/watcher-store");
|
|
|
68
69
|
// overflows has passed. Probe often; a failed probe just drops back to polling.
|
|
69
70
|
const FSEVENTS_RECOVERY_INTERVAL_MS = 15 * 60 * 1000;
|
|
70
71
|
const FSEVENTS_HEALTH_WINDOW_MS = 5 * 60 * 1000; // 5 min of quiet = "healthy"
|
|
71
|
-
//
|
|
72
|
+
// Terminal subscription failures can arrive in bursts. Every
|
|
72
73
|
// recovery inside this window folds into one deferred catchup scan instead of
|
|
73
74
|
// each rescanning the project (~25s on a 26k-file tree) or skipping outright
|
|
74
75
|
// and leaving its drop window uncovered.
|
|
@@ -104,18 +105,21 @@ class WatcherManager {
|
|
|
104
105
|
this.pollIntervals = new Map();
|
|
105
106
|
this.pollRecoveryTimers = new Map();
|
|
106
107
|
this.recoveryTimeouts = new Map();
|
|
107
|
-
this.
|
|
108
|
+
this.lastWatcherFailureMs = new Map();
|
|
108
109
|
this.lastWatcherErrorLogMs = new Map();
|
|
109
110
|
this.overflowCounts = new Map();
|
|
110
111
|
this.reconciledAt = new Map();
|
|
111
112
|
this.catchupDurations = new Map();
|
|
112
113
|
this.lastCatchupEndMs = new Map();
|
|
114
|
+
this.lastCatchupStartMs = new Map();
|
|
115
|
+
this.overflowPendingRoots = new Set();
|
|
113
116
|
this.deferredCatchups = new Map();
|
|
114
117
|
this.catchups = new Map();
|
|
115
118
|
this.degradedRoots = new Set();
|
|
116
119
|
this.terminalFailures = new Map();
|
|
117
120
|
this.watchLifecycles = new Map();
|
|
118
121
|
this.subscriptionUpdates = new Map();
|
|
122
|
+
this.watcherGenerations = new Map();
|
|
119
123
|
}
|
|
120
124
|
health(root) {
|
|
121
125
|
var _a;
|
|
@@ -126,7 +130,8 @@ class WatcherManager {
|
|
|
126
130
|
watcherMode: this.pollIntervals.has(root)
|
|
127
131
|
? "polling"
|
|
128
132
|
: this.pendingOps.has(`recover:${root}`) ||
|
|
129
|
-
this.deferredCatchups.has(root)
|
|
133
|
+
this.deferredCatchups.has(root) ||
|
|
134
|
+
this.overflowPendingRoots.has(root)
|
|
130
135
|
? "recovering"
|
|
131
136
|
: "native",
|
|
132
137
|
catchupRunning: this.catchups.has(root),
|
|
@@ -250,7 +255,7 @@ class WatcherManager {
|
|
|
250
255
|
const name = path.basename(root);
|
|
251
256
|
console.error(`[daemon:${name}] Subscribe failed at startup (${err instanceof Error ? err.message : err}) — switching to poll mode`);
|
|
252
257
|
this.watcherFailCount.set(root, 1000); // > MAX_WATCHER_RETRIES
|
|
253
|
-
this.
|
|
258
|
+
this.lastWatcherFailureMs.set(root, Date.now());
|
|
254
259
|
this.recoverWatcher(root, processor);
|
|
255
260
|
}
|
|
256
261
|
if (!ownsLifecycle())
|
|
@@ -311,6 +316,8 @@ class WatcherManager {
|
|
|
311
316
|
if (this.deps.processors.get(root) !== processor ||
|
|
312
317
|
((_b = (_a = this.deps).getShuttingDown) === null || _b === void 0 ? void 0 : _b.call(_a)))
|
|
313
318
|
return;
|
|
319
|
+
const generation = Symbol(root);
|
|
320
|
+
this.watcherGenerations.set(root, generation);
|
|
314
321
|
// Unsubscribe existing watcher if any (e.g. during recovery)
|
|
315
322
|
const existingSub = this.deps.subscriptions.get(root);
|
|
316
323
|
if (existingSub) {
|
|
@@ -322,21 +329,30 @@ class WatcherManager {
|
|
|
322
329
|
}
|
|
323
330
|
const sub = yield watcher.subscribe(root, (err, events) => {
|
|
324
331
|
var _a, _b;
|
|
325
|
-
|
|
332
|
+
var _c, _d;
|
|
333
|
+
if (this.deps.processors.get(root) !== processor ||
|
|
334
|
+
this.watcherGenerations.get(root) !== generation ||
|
|
335
|
+
((_b = (_a = this.deps).getShuttingDown) === null || _b === void 0 ? void 0 : _b.call(_a)))
|
|
326
336
|
return;
|
|
327
337
|
if (err) {
|
|
328
|
-
this.overflowCounts.set(root, ((
|
|
338
|
+
this.overflowCounts.set(root, ((_c = this.overflowCounts.get(root)) !== null && _c !== void 0 ? _c : 0) + 1);
|
|
329
339
|
const now = Date.now();
|
|
330
340
|
if (!this.lastWatcherErrorLogMs.has(root) ||
|
|
331
341
|
now - this.lastWatcherErrorLogMs.get(root) >= 60000) {
|
|
332
342
|
this.lastWatcherErrorLogMs.set(root, now);
|
|
333
343
|
console.error(`[daemon:${name}] Watcher error (total=${this.overflowCounts.get(root)}):`, err);
|
|
334
344
|
}
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
if ((0, watcher_errors_1.isFSEventsGap)(err)) {
|
|
346
|
+
this.overflowPendingRoots.add(root);
|
|
347
|
+
this.requestOverflowCatchup(root, processor);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
this.recoverWatcher(root, processor);
|
|
351
|
+
}
|
|
337
352
|
}
|
|
338
|
-
//
|
|
339
|
-
|
|
353
|
+
// Gaps do not kill Parcel's stream. Only a terminal failure should
|
|
354
|
+
// reset the subscription health window or prevent leaving poll mode.
|
|
355
|
+
const lastOverflow = (_d = this.lastWatcherFailureMs.get(root)) !== null && _d !== void 0 ? _d : 0;
|
|
340
356
|
if (Date.now() - lastOverflow > 5 * 60 * 1000) {
|
|
341
357
|
this.watcherFailCount.delete(root);
|
|
342
358
|
}
|
|
@@ -350,13 +366,30 @@ class WatcherManager {
|
|
|
350
366
|
...(yield (0, watcher_ignore_1.readProjectWatcherIgnores)(root)),
|
|
351
367
|
],
|
|
352
368
|
});
|
|
353
|
-
if (this.deps.processors.get(root) !== processor
|
|
369
|
+
if (this.deps.processors.get(root) !== processor ||
|
|
370
|
+
this.watcherGenerations.get(root) !== generation) {
|
|
354
371
|
yield sub.unsubscribe();
|
|
355
372
|
return;
|
|
356
373
|
}
|
|
357
374
|
this.deps.subscriptions.set(root, sub);
|
|
358
375
|
});
|
|
359
376
|
}
|
|
377
|
+
requestOverflowCatchup(root, processor) {
|
|
378
|
+
const started = this.lastCatchupStartMs.get(root);
|
|
379
|
+
const remaining = started === undefined
|
|
380
|
+
? 0
|
|
381
|
+
: watcher_errors_1.FSEVENTS_GAP_SCAN_INTERVAL_MS - (Date.now() - started);
|
|
382
|
+
// A gap during a scan may concern a path already visited. Keep one later
|
|
383
|
+
// scan rather than treating the current walk as coverage of that gap.
|
|
384
|
+
if (this.catchups.has(root) || remaining > 0) {
|
|
385
|
+
this.deferCatchup(root, processor, Math.max(remaining, watcher_errors_1.FSEVENTS_GAP_SCAN_INTERVAL_MS));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.cancelDeferredCatchup(root);
|
|
389
|
+
this.runCatchup(root, processor).catch((err) => {
|
|
390
|
+
console.error(`[daemon:${path.basename(root)}] Overflow catchup failed:`, err);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
360
393
|
recoverWatcher(root, processor) {
|
|
361
394
|
var _a;
|
|
362
395
|
const name = path.basename(root);
|
|
@@ -370,11 +403,11 @@ class WatcherManager {
|
|
|
370
403
|
this.pendingOps.add(recoveryKey);
|
|
371
404
|
const fails = ((_a = this.watcherFailCount.get(root)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
372
405
|
this.watcherFailCount.set(root, fails);
|
|
373
|
-
this.
|
|
406
|
+
this.lastWatcherFailureMs.set(root, Date.now());
|
|
374
407
|
const MAX_WATCHER_RETRIES = 3;
|
|
375
408
|
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
376
409
|
if (fails > MAX_WATCHER_RETRIES) {
|
|
377
|
-
//
|
|
410
|
+
// Repeated terminal subscription failures require periodic catchup scans.
|
|
378
411
|
// Always tear down the broken sub, even if poll mode is already active —
|
|
379
412
|
// this can happen if a recovery attempt resubscribed successfully then
|
|
380
413
|
// re-overflowed during the 5-min health window.
|
|
@@ -384,7 +417,7 @@ class WatcherManager {
|
|
|
384
417
|
this.deps.subscriptions.delete(root);
|
|
385
418
|
}
|
|
386
419
|
if (!this.pollIntervals.has(root)) {
|
|
387
|
-
console.error(`[daemon:${name}]
|
|
420
|
+
console.error(`[daemon:${name}] Native watcher failed after ${fails} terminal errors — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
|
|
388
421
|
// Run an immediate catchup, then schedule periodic ones
|
|
389
422
|
this.runCatchup(root, processor).catch((err) => {
|
|
390
423
|
console.error(`[daemon:${name}] Poll catchup failed:`, err);
|
|
@@ -416,7 +449,7 @@ class WatcherManager {
|
|
|
416
449
|
}
|
|
417
450
|
// Backoff: wait before re-subscribing (3s, 6s, 12s)
|
|
418
451
|
const delayMs = 3000 * Math.pow(2, (fails - 1));
|
|
419
|
-
console.error(`[daemon:${name}] Recovering watcher (attempt ${fails}/${MAX_WATCHER_RETRIES}, backoff ${delayMs}ms)...`);
|
|
452
|
+
console.error(`[daemon:${name}] Recovering failed watcher (attempt ${fails}/${MAX_WATCHER_RETRIES}, backoff ${delayMs}ms)...`);
|
|
420
453
|
const timeout = setTimeout(() => {
|
|
421
454
|
this.recoveryTimeouts.delete(root);
|
|
422
455
|
if (this.deps.getShuttingDown() ||
|
|
@@ -520,14 +553,14 @@ class WatcherManager {
|
|
|
520
553
|
if (this.deps.processors.get(root) !== processor)
|
|
521
554
|
return;
|
|
522
555
|
// Wait one health window — if the new subscription survives without
|
|
523
|
-
// another
|
|
556
|
+
// another terminal failure, we consider it recovered and tear down poll mode.
|
|
524
557
|
yield abortableDelay(FSEVENTS_HEALTH_WINDOW_MS, lifecycle.signal);
|
|
525
558
|
if (this.deps.getShuttingDown() ||
|
|
526
559
|
this.deps.processors.get(root) !== processor)
|
|
527
560
|
return;
|
|
528
|
-
const lastOverflow = (_a = this.
|
|
561
|
+
const lastOverflow = (_a = this.lastWatcherFailureMs.get(root)) !== null && _a !== void 0 ? _a : 0;
|
|
529
562
|
if (Date.now() - lastOverflow < FSEVENTS_HEALTH_WINDOW_MS) {
|
|
530
|
-
console.log(`[daemon:${name}] FSEvents recovery aborted —
|
|
563
|
+
console.log(`[daemon:${name}] FSEvents recovery aborted — terminal failure within health window, staying in poll mode`);
|
|
531
564
|
return; // recoverWatcher will have re-armed poll mode if needed
|
|
532
565
|
}
|
|
533
566
|
// Healthy — drop poll mode.
|
|
@@ -555,8 +588,9 @@ class WatcherManager {
|
|
|
555
588
|
return __awaiter(this, void 0, void 0, function* () {
|
|
556
589
|
var _a, e_1, _b, _c;
|
|
557
590
|
var _d;
|
|
558
|
-
var _e;
|
|
591
|
+
var _e, _f, _g;
|
|
559
592
|
const scanStart = Date.now();
|
|
593
|
+
const overflowCountAtStart = (_e = this.overflowCounts.get(root)) !== null && _e !== void 0 ? _e : 0;
|
|
560
594
|
const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
|
|
561
595
|
const metaCache = this.deps.getMetaCache();
|
|
562
596
|
const vectorDb = this.deps.getVectorDb();
|
|
@@ -573,12 +607,12 @@ class WatcherManager {
|
|
|
573
607
|
let skipped = 0;
|
|
574
608
|
let debugSamples = 0;
|
|
575
609
|
try {
|
|
576
|
-
for (var
|
|
610
|
+
for (var _h = true, _j = __asyncValues((0, walker_1.walk)(root, {
|
|
577
611
|
policy: processor.filePolicy,
|
|
578
612
|
state: walkState,
|
|
579
|
-
})),
|
|
580
|
-
_c =
|
|
581
|
-
|
|
613
|
+
})), _k; _k = yield _j.next(), _a = _k.done, !_a; _h = true) {
|
|
614
|
+
_c = _k.value;
|
|
615
|
+
_h = false;
|
|
582
616
|
const relPath = _c;
|
|
583
617
|
if (signal.aborted)
|
|
584
618
|
return false;
|
|
@@ -606,6 +640,7 @@ class WatcherManager {
|
|
|
606
640
|
else if (reconciliation.action === "reprocess") {
|
|
607
641
|
processor.handleFileEvent("change", absPath, {
|
|
608
642
|
forceReprocess: true,
|
|
643
|
+
workKind: "catchup",
|
|
609
644
|
});
|
|
610
645
|
queued++;
|
|
611
646
|
if (queued % 500 === 0) {
|
|
@@ -623,7 +658,9 @@ class WatcherManager {
|
|
|
623
658
|
});
|
|
624
659
|
if (snapshot.size !== stats.size ||
|
|
625
660
|
snapshot.mtimeMs !== stats.mtimeMs) {
|
|
626
|
-
processor.handleFileEvent("change", absPath
|
|
661
|
+
processor.handleFileEvent("change", absPath, {
|
|
662
|
+
workKind: "catchup",
|
|
663
|
+
});
|
|
627
664
|
continue;
|
|
628
665
|
}
|
|
629
666
|
const hash = (0, file_utils_1.computeContentHash)(snapshot.buffer, absPath);
|
|
@@ -643,7 +680,7 @@ class WatcherManager {
|
|
|
643
680
|
}
|
|
644
681
|
if (signal.aborted)
|
|
645
682
|
return false;
|
|
646
|
-
processor.handleFileEvent("change", absPath);
|
|
683
|
+
processor.handleFileEvent("change", absPath, { workKind: "catchup" });
|
|
647
684
|
queued++;
|
|
648
685
|
// Throttle: pause periodically during large catchup scans to let the
|
|
649
686
|
// batch processor drain and compaction run between bursts.
|
|
@@ -665,7 +702,7 @@ class WatcherManager {
|
|
|
665
702
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
666
703
|
finally {
|
|
667
704
|
try {
|
|
668
|
-
if (!
|
|
705
|
+
if (!_h && !_a && (_b = _j.return)) yield _b.call(_j);
|
|
669
706
|
}
|
|
670
707
|
finally { if (e_1) throw e_1.error; }
|
|
671
708
|
}
|
|
@@ -677,7 +714,10 @@ class WatcherManager {
|
|
|
677
714
|
return false;
|
|
678
715
|
if (!seenPaths.has(cachedPath) &&
|
|
679
716
|
!(0, walker_1.isPathProtectedByWalkState)(cachedPath, walkState)) {
|
|
680
|
-
processor.handleFileEvent("unlink", cachedPath, {
|
|
717
|
+
processor.handleFileEvent("unlink", cachedPath, {
|
|
718
|
+
forceDelete: true,
|
|
719
|
+
workKind: "cleanup",
|
|
720
|
+
});
|
|
681
721
|
purged++;
|
|
682
722
|
}
|
|
683
723
|
}
|
|
@@ -695,7 +735,7 @@ class WatcherManager {
|
|
|
695
735
|
}
|
|
696
736
|
else {
|
|
697
737
|
this.degradedRoots.delete(root);
|
|
698
|
-
const failedFiles = (
|
|
738
|
+
const failedFiles = (_f = (_d = this.terminalFailures.get(root)) === null || _d === void 0 ? void 0 : _d.size) !== null && _f !== void 0 ? _f : 0;
|
|
699
739
|
(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
|
|
700
740
|
? { lastError: `${failedFiles} file(s) exhausted automatic retries` }
|
|
701
741
|
: {})));
|
|
@@ -711,8 +751,12 @@ class WatcherManager {
|
|
|
711
751
|
const ended = Date.now();
|
|
712
752
|
this.lastCatchupEndMs.set(root, ended);
|
|
713
753
|
this.catchupDurations.set(root, ended - scanStart);
|
|
714
|
-
if (complete)
|
|
754
|
+
if (complete) {
|
|
715
755
|
this.reconciledAt.set(root, ended);
|
|
756
|
+
// A later gap still needs its deferred scan, even if this walk succeeded.
|
|
757
|
+
if (((_g = this.overflowCounts.get(root)) !== null && _g !== void 0 ? _g : 0) === overflowCountAtStart)
|
|
758
|
+
this.overflowPendingRoots.delete(root);
|
|
759
|
+
}
|
|
716
760
|
return complete;
|
|
717
761
|
});
|
|
718
762
|
}
|
|
@@ -731,6 +775,7 @@ class WatcherManager {
|
|
|
731
775
|
let incompleteRetries = 0;
|
|
732
776
|
do {
|
|
733
777
|
active.dirty = false;
|
|
778
|
+
this.lastCatchupStartMs.set(root, Date.now());
|
|
734
779
|
const complete = yield this.catchupScan(root, processor, signal);
|
|
735
780
|
if (!complete && !signal.aborted && incompleteRetries < 3) {
|
|
736
781
|
incompleteRetries++;
|
|
@@ -769,6 +814,7 @@ class WatcherManager {
|
|
|
769
814
|
// independently of the processor, so clear them even on the early return.
|
|
770
815
|
(_a = this.watchLifecycles.get(root)) === null || _a === void 0 ? void 0 : _a.abort();
|
|
771
816
|
this.watchLifecycles.delete(root);
|
|
817
|
+
this.watcherGenerations.delete(root);
|
|
772
818
|
const pollInterval = this.pollIntervals.get(root);
|
|
773
819
|
if (pollInterval) {
|
|
774
820
|
clearInterval(pollInterval);
|
|
@@ -798,8 +844,10 @@ class WatcherManager {
|
|
|
798
844
|
this.deps.subscriptions.delete(root);
|
|
799
845
|
}
|
|
800
846
|
this.deps.evictSearcher(root);
|
|
801
|
-
this.
|
|
847
|
+
this.lastWatcherFailureMs.delete(root);
|
|
802
848
|
this.lastCatchupEndMs.delete(root);
|
|
849
|
+
this.lastCatchupStartMs.delete(root);
|
|
850
|
+
this.overflowPendingRoots.delete(root);
|
|
803
851
|
this.degradedRoots.delete(root);
|
|
804
852
|
this.terminalFailures.delete(root);
|
|
805
853
|
(0, watcher_store_1.unregisterWatcherByRoot)(root);
|
|
@@ -58,6 +58,10 @@ const watcher_batch_1 = require("./watcher-batch");
|
|
|
58
58
|
const DEBOUNCE_MS = 2000;
|
|
59
59
|
const MAX_RETRIES = 5;
|
|
60
60
|
const MAX_BATCH_SIZE = 50;
|
|
61
|
+
const MAX_CATCHUP_BATCH_SIZE = 4;
|
|
62
|
+
const BACKGROUND_DISPATCH_WINDOW_MS = 2000;
|
|
63
|
+
const MAX_CONSECUTIVE_LIVE_BATCHES = 3;
|
|
64
|
+
const WORK_PRIORITY = { live: 0, catchup: 1, cleanup: 2 };
|
|
61
65
|
// Pool priority cannot preempt an active processFile, so keep one worker free
|
|
62
66
|
// for latency-sensitive encodeQuery/rerank work during indexing bursts.
|
|
63
67
|
const DEFAULT_BATCH_CONCURRENCY = Math.max(1, config_1.CONFIG.WORKER_THREADS - 1);
|
|
@@ -70,6 +74,10 @@ class ProjectBatchProcessor {
|
|
|
70
74
|
constructor(opts) {
|
|
71
75
|
var _a, _b, _c;
|
|
72
76
|
this.pending = new Map();
|
|
77
|
+
this.pendingWork = new Map();
|
|
78
|
+
this.activeWork = new Map();
|
|
79
|
+
this.liveBatches = 0;
|
|
80
|
+
this.nextBackground = "catchup";
|
|
73
81
|
this.retryCount = new Map();
|
|
74
82
|
this.retryAt = new Map();
|
|
75
83
|
this.terminalFailures = new Set();
|
|
@@ -110,6 +118,7 @@ class ProjectBatchProcessor {
|
|
|
110
118
|
}
|
|
111
119
|
handleFileEvent(event, absPath, options) {
|
|
112
120
|
var _a, _b;
|
|
121
|
+
var _c;
|
|
113
122
|
if (this.closed)
|
|
114
123
|
return;
|
|
115
124
|
const normalize = this.filePolicy.normalizeEventPath;
|
|
@@ -149,10 +158,35 @@ class ProjectBatchProcessor {
|
|
|
149
158
|
this.forcedReprocess.set(normalized, ++this.forceGeneration);
|
|
150
159
|
}
|
|
151
160
|
this.retryAt.delete(normalized);
|
|
152
|
-
this.
|
|
161
|
+
this.enqueueWork(normalized, event, {
|
|
162
|
+
kind: (_c = options === null || options === void 0 ? void 0 : options.workKind) !== null && _c !== void 0 ? _c : "live",
|
|
163
|
+
queuedAt: Date.now(),
|
|
164
|
+
});
|
|
153
165
|
(_b = this.onActivity) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
154
166
|
this.scheduleBatch();
|
|
155
167
|
}
|
|
168
|
+
enqueueWork(absPath, event, work) {
|
|
169
|
+
var _a;
|
|
170
|
+
const previous = (_a = this.pendingWork.get(absPath)) !== null && _a !== void 0 ? _a : this.activeWork.get(absPath);
|
|
171
|
+
this.pending.set(absPath, event);
|
|
172
|
+
this.pendingWork.set(absPath, !previous || WORK_PRIORITY[work.kind] < WORK_PRIORITY[previous.kind]
|
|
173
|
+
? work
|
|
174
|
+
: {
|
|
175
|
+
kind: previous.kind,
|
|
176
|
+
queuedAt: previous.kind === work.kind
|
|
177
|
+
? Math.min(previous.queuedAt, work.queuedAt)
|
|
178
|
+
: previous.queuedAt,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
hasReadyLiveWork() {
|
|
182
|
+
var _a;
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
for (const [file, work] of this.pendingWork) {
|
|
185
|
+
if (work.kind === "live" && ((_a = this.retryAt.get(file)) !== null && _a !== void 0 ? _a : 0) <= now)
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
156
190
|
/** Live (re)index progress: files queued + whether a batch is running.
|
|
157
191
|
*
|
|
158
192
|
* recentFiles/recentReindexed summarize the last few batches so callers can
|
|
@@ -162,18 +196,40 @@ class ProjectBatchProcessor {
|
|
|
162
196
|
* not pending work. Without this, a large pendingFiles alone reads as "the
|
|
163
197
|
* index is far behind" and search callers caveat correct results. */
|
|
164
198
|
get progress() {
|
|
199
|
+
var _a;
|
|
165
200
|
let recentFiles = 0;
|
|
166
201
|
let recentReindexed = 0;
|
|
167
202
|
for (const b of this.recentBatches) {
|
|
168
203
|
recentFiles += b.files;
|
|
169
204
|
recentReindexed += b.reindexed;
|
|
170
205
|
}
|
|
206
|
+
const queue = {
|
|
207
|
+
live: 0,
|
|
208
|
+
catchup: 0,
|
|
209
|
+
cleanup: 0,
|
|
210
|
+
activeFiles: this.activeWork.size,
|
|
211
|
+
oldestLiveEditAgeMs: null,
|
|
212
|
+
};
|
|
213
|
+
let oldest = Number.POSITIVE_INFINITY;
|
|
214
|
+
for (const file of this.pending.keys()) {
|
|
215
|
+
const work = this.pendingWork.get(file);
|
|
216
|
+
queue[(_a = work === null || work === void 0 ? void 0 : work.kind) !== null && _a !== void 0 ? _a : "live"]++;
|
|
217
|
+
if ((work === null || work === void 0 ? void 0 : work.kind) === "live")
|
|
218
|
+
oldest = Math.min(oldest, work.queuedAt);
|
|
219
|
+
}
|
|
220
|
+
for (const work of this.activeWork.values()) {
|
|
221
|
+
if (work.kind === "live")
|
|
222
|
+
oldest = Math.min(oldest, work.queuedAt);
|
|
223
|
+
}
|
|
224
|
+
if (Number.isFinite(oldest))
|
|
225
|
+
queue.oldestLiveEditAgeMs = Math.max(0, Date.now() - oldest);
|
|
171
226
|
return {
|
|
172
227
|
pendingFiles: this.pending.size,
|
|
173
228
|
processing: this.processing,
|
|
174
229
|
failedFiles: this.terminalFailures.size,
|
|
175
230
|
recentFiles,
|
|
176
231
|
recentReindexed,
|
|
232
|
+
queue,
|
|
177
233
|
};
|
|
178
234
|
}
|
|
179
235
|
loadReusableEmbeddings(absPath) {
|
|
@@ -253,8 +309,8 @@ class ProjectBatchProcessor {
|
|
|
253
309
|
}
|
|
254
310
|
processBatch(operationSignal) {
|
|
255
311
|
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
-
var _a, _b, _c, _d;
|
|
257
|
-
var
|
|
312
|
+
var _a, _b, _c, _d, _e;
|
|
313
|
+
var _f, _g, _h, _j;
|
|
258
314
|
if (this.closed || this.processing || this.pending.size === 0)
|
|
259
315
|
return;
|
|
260
316
|
const diskPressure = this.vectorDb.checkDiskPressure();
|
|
@@ -263,25 +319,55 @@ class ProjectBatchProcessor {
|
|
|
263
319
|
}
|
|
264
320
|
const batch = new Map();
|
|
265
321
|
const batchForceGenerations = new Map();
|
|
322
|
+
const batchWork = new Map();
|
|
266
323
|
const now = Date.now();
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
324
|
+
const ready = {
|
|
325
|
+
live: [],
|
|
326
|
+
catchup: [],
|
|
327
|
+
cleanup: [],
|
|
328
|
+
};
|
|
329
|
+
for (const absPath of this.pending.keys()) {
|
|
330
|
+
if (((_f = this.retryAt.get(absPath)) !== null && _f !== void 0 ? _f : 0) > now)
|
|
270
331
|
continue;
|
|
332
|
+
const kind = (_g = (_a = this.pendingWork.get(absPath)) === null || _a === void 0 ? void 0 : _a.kind) !== null && _g !== void 0 ? _g : "live";
|
|
333
|
+
if (ready[kind].length < MAX_BATCH_SIZE)
|
|
334
|
+
ready[kind].push(absPath);
|
|
335
|
+
}
|
|
336
|
+
const background = ready[this.nextBackground].length
|
|
337
|
+
? this.nextBackground
|
|
338
|
+
: this.nextBackground === "catchup"
|
|
339
|
+
? "cleanup"
|
|
340
|
+
: "catchup";
|
|
341
|
+
const fairBackground = ready.live.length > 0 &&
|
|
342
|
+
ready[background].length > 0 &&
|
|
343
|
+
this.liveBatches >= MAX_CONSECUTIVE_LIVE_BATCHES;
|
|
344
|
+
const kind = ready.live.length > 0 && !fairBackground ? "live" : background;
|
|
345
|
+
const limit = fairBackground
|
|
346
|
+
? 1
|
|
347
|
+
: kind === "catchup"
|
|
348
|
+
? MAX_CATCHUP_BATCH_SIZE
|
|
349
|
+
: MAX_BATCH_SIZE;
|
|
350
|
+
for (const absPath of ready[kind].slice(0, limit)) {
|
|
351
|
+
const event = this.pending.get(absPath);
|
|
271
352
|
batch.set(absPath, event);
|
|
353
|
+
batchWork.set(absPath, (_h = this.pendingWork.get(absPath)) !== null && _h !== void 0 ? _h : { kind, queuedAt: now });
|
|
272
354
|
const forceGeneration = this.forcedReprocess.get(absPath);
|
|
273
355
|
if (forceGeneration !== undefined) {
|
|
274
356
|
batchForceGenerations.set(absPath, forceGeneration);
|
|
275
357
|
}
|
|
276
|
-
taken++;
|
|
277
|
-
if (taken >= MAX_BATCH_SIZE)
|
|
278
|
-
break;
|
|
279
358
|
}
|
|
280
359
|
if (batch.size === 0) {
|
|
281
360
|
this.schedulePendingBatch(30000);
|
|
282
361
|
return;
|
|
283
362
|
}
|
|
284
363
|
this.processing = true;
|
|
364
|
+
this.activeWork = batchWork;
|
|
365
|
+
if (kind === "live")
|
|
366
|
+
this.liveBatches++;
|
|
367
|
+
else {
|
|
368
|
+
this.liveBatches = 0;
|
|
369
|
+
this.nextBackground = kind === "catchup" ? "cleanup" : "catchup";
|
|
370
|
+
}
|
|
285
371
|
const batchAc = new AbortController();
|
|
286
372
|
const abortBatch = () => batchAc.abort(operationSignal.reason);
|
|
287
373
|
if (operationSignal.aborted)
|
|
@@ -295,6 +381,7 @@ class ProjectBatchProcessor {
|
|
|
295
381
|
}, this.batchTimeoutMs);
|
|
296
382
|
for (const key of batch.keys()) {
|
|
297
383
|
this.pending.delete(key);
|
|
384
|
+
this.pendingWork.delete(key);
|
|
298
385
|
this.retryAt.delete(key);
|
|
299
386
|
}
|
|
300
387
|
const filenames = [...batch.keys()].map((p) => path.basename(p));
|
|
@@ -331,7 +418,7 @@ class ProjectBatchProcessor {
|
|
|
331
418
|
this.retryCount.set(absPath, retry.failures);
|
|
332
419
|
this.retryAt.set(absPath, Date.now() + retry.backoffMs);
|
|
333
420
|
}
|
|
334
|
-
this.
|
|
421
|
+
this.enqueueWork(absPath, event, batchWork.get(absPath));
|
|
335
422
|
};
|
|
336
423
|
let stopDispatch = false;
|
|
337
424
|
const processOne = (absPath, event) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -473,7 +560,7 @@ class ProjectBatchProcessor {
|
|
|
473
560
|
}
|
|
474
561
|
}
|
|
475
562
|
});
|
|
476
|
-
const batchConcurrency = Math.min(this.concurrency, Math.max(1, Math.ceil(batch.size / FILES_PER_BATCH_SLOT)));
|
|
563
|
+
const batchConcurrency = Math.min(kind === "live" ? this.concurrency : 1, Math.max(1, Math.ceil(batch.size / FILES_PER_BATCH_SLOT)));
|
|
477
564
|
const activeTasks = [];
|
|
478
565
|
const schedule = (task) => __awaiter(this, void 0, void 0, function* () {
|
|
479
566
|
const taskPromise = task();
|
|
@@ -491,6 +578,14 @@ class ProjectBatchProcessor {
|
|
|
491
578
|
for (const [absPath, event] of batch) {
|
|
492
579
|
if (batchAc.signal.aborted || stopDispatch)
|
|
493
580
|
break;
|
|
581
|
+
// Do not enqueue a whole background batch into the worker pool. Finish
|
|
582
|
+
// the current file, commit its results, then admit waiting live edits.
|
|
583
|
+
// A single fairness file still runs during continuous live traffic.
|
|
584
|
+
if (kind !== "live" &&
|
|
585
|
+
settled > 0 &&
|
|
586
|
+
(Date.now() - start >= BACKGROUND_DISPATCH_WINDOW_MS ||
|
|
587
|
+
(!fairBackground && this.hasReadyLiveWork())))
|
|
588
|
+
break;
|
|
494
589
|
yield schedule(() => processOne(absPath, event));
|
|
495
590
|
}
|
|
496
591
|
yield Promise.allSettled(activeTasks);
|
|
@@ -561,9 +656,9 @@ class ProjectBatchProcessor {
|
|
|
561
656
|
}
|
|
562
657
|
const duration = Date.now() - start;
|
|
563
658
|
if (reindexed > 0) {
|
|
564
|
-
(
|
|
659
|
+
(_b = this.onReindex) === null || _b === void 0 ? void 0 : _b.call(this, reindexed, duration);
|
|
565
660
|
}
|
|
566
|
-
this.recentBatches.push({ files:
|
|
661
|
+
this.recentBatches.push({ files: settled, reindexed });
|
|
567
662
|
if (this.recentBatches.length > ProjectBatchProcessor.RECENT_BATCH_WINDOW) {
|
|
568
663
|
this.recentBatches.shift();
|
|
569
664
|
}
|
|
@@ -578,7 +673,7 @@ class ProjectBatchProcessor {
|
|
|
578
673
|
this.retryCount.delete(absPath);
|
|
579
674
|
this.retryAt.delete(absPath);
|
|
580
675
|
this.terminalFailures.delete(absPath);
|
|
581
|
-
(
|
|
676
|
+
(_c = this.onPathSuccess) === null || _c === void 0 ? void 0 : _c.call(this, absPath);
|
|
582
677
|
}
|
|
583
678
|
// Trigger compaction if fragments are accumulating
|
|
584
679
|
if (reindexed > 0 && diskPressure !== "critical") {
|
|
@@ -599,7 +694,7 @@ class ProjectBatchProcessor {
|
|
|
599
694
|
if (this.terminalFailures.has(absPath))
|
|
600
695
|
continue;
|
|
601
696
|
if (!this.pending.has(absPath)) {
|
|
602
|
-
this.
|
|
697
|
+
this.enqueueWork(absPath, event, batchWork.get(absPath));
|
|
603
698
|
}
|
|
604
699
|
}
|
|
605
700
|
(0, logger_1.log)(this.wtag, "Disk pressure — requeued batch, will retry in 60s");
|
|
@@ -622,7 +717,7 @@ class ProjectBatchProcessor {
|
|
|
622
717
|
if (this.terminalFailures.has(absPath))
|
|
623
718
|
continue;
|
|
624
719
|
if (!this.pending.has(absPath))
|
|
625
|
-
this.
|
|
720
|
+
this.enqueueWork(absPath, event, batchWork.get(absPath));
|
|
626
721
|
}
|
|
627
722
|
backoffOverrideMs = 30 * 60 * 1000;
|
|
628
723
|
}
|
|
@@ -635,19 +730,19 @@ class ProjectBatchProcessor {
|
|
|
635
730
|
continue;
|
|
636
731
|
if (this.pending.has(absPath))
|
|
637
732
|
continue;
|
|
638
|
-
const retry = (0, watcher_batch_1.computePathRetry)((
|
|
733
|
+
const retry = (0, watcher_batch_1.computePathRetry)((_j = this.retryCount.get(absPath)) !== null && _j !== void 0 ? _j : 0, MAX_RETRIES, DEBOUNCE_MS);
|
|
639
734
|
if (!retry.retry) {
|
|
640
735
|
this.retryCount.delete(absPath);
|
|
641
736
|
this.retryAt.delete(absPath);
|
|
642
737
|
this.terminalFailures.add(absPath);
|
|
643
|
-
(
|
|
738
|
+
(_d = this.onTerminalFailure) === null || _d === void 0 ? void 0 : _d.call(this, absPath);
|
|
644
739
|
dropped++;
|
|
645
740
|
droppedPaths.push(absPath);
|
|
646
741
|
continue;
|
|
647
742
|
}
|
|
648
743
|
this.retryCount.set(absPath, retry.failures);
|
|
649
744
|
this.retryAt.set(absPath, Date.now() + retry.backoffMs);
|
|
650
|
-
this.
|
|
745
|
+
this.enqueueWork(absPath, event, batchWork.get(absPath));
|
|
651
746
|
}
|
|
652
747
|
if (dropped > 0) {
|
|
653
748
|
(0, logger_1.log)(this.wtag, `Dropped ${dropped} file(s) after ${MAX_RETRIES} retries: ${droppedPaths.map((p) => path.basename(p)).join(", ")}`);
|
|
@@ -666,10 +761,11 @@ class ProjectBatchProcessor {
|
|
|
666
761
|
if (!this.pending.has(absPath)) {
|
|
667
762
|
this.retryCount.delete(absPath);
|
|
668
763
|
this.retryAt.delete(absPath);
|
|
669
|
-
this.
|
|
764
|
+
this.enqueueWork(absPath, "change", batchWork.get(absPath));
|
|
670
765
|
}
|
|
671
766
|
}
|
|
672
767
|
}
|
|
768
|
+
this.activeWork = new Map();
|
|
673
769
|
if (!this.closed && this.pending.size > 0) {
|
|
674
770
|
if (backoffOverrideMs > 0) {
|
|
675
771
|
if (this.debounceTimer)
|
|
@@ -686,7 +782,7 @@ class ProjectBatchProcessor {
|
|
|
686
782
|
}
|
|
687
783
|
}
|
|
688
784
|
if (!this.closed)
|
|
689
|
-
(
|
|
785
|
+
(_e = this.onBatchSettled) === null || _e === void 0 ? void 0 : _e.call(this);
|
|
690
786
|
}
|
|
691
787
|
});
|
|
692
788
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FSEVENTS_GAP_SCAN_INTERVAL_MS = void 0;
|
|
4
|
+
exports.isFSEventsGap = isFSEventsGap;
|
|
5
|
+
/** Parcel reports these gaps without closing its FSEvents stream. Other errors
|
|
6
|
+
* may clear the native callbacks and still require subscription recovery. */
|
|
7
|
+
function isFSEventsGap(error) {
|
|
8
|
+
return (error.message ===
|
|
9
|
+
"Events were dropped by the FSEvents client. File system must be re-scanned." ||
|
|
10
|
+
error.message ===
|
|
11
|
+
"Events were dropped by the kernel. File system must be re-scanned." ||
|
|
12
|
+
error.message === "Too many events. File system must be re-scanned.");
|
|
13
|
+
}
|
|
14
|
+
// Bound reconciliation under sustained churn without abandoning native edits
|
|
15
|
+
// for the five-minute polling fallback used by a failed subscription.
|
|
16
|
+
exports.FSEVENTS_GAP_SCAN_INTERVAL_MS = 30000;
|
|
@@ -58,6 +58,7 @@ const cache_coherence_1 = require("./cache-coherence");
|
|
|
58
58
|
const file_policy_1 = require("./file-policy");
|
|
59
59
|
const ignore_patterns_1 = require("./ignore-patterns");
|
|
60
60
|
const walker_1 = require("./walker");
|
|
61
|
+
const watcher_errors_1 = require("./watcher-errors");
|
|
61
62
|
const watcher_ignore_1 = require("./watcher-ignore");
|
|
62
63
|
// Directory names ignored at ANY depth. @parcel/watcher treats a non-glob
|
|
63
64
|
// ignore entry as a literal path resolved against the watch root, so a bare
|
|
@@ -125,6 +126,11 @@ function startWatcher(opts) {
|
|
|
125
126
|
let subscription = null;
|
|
126
127
|
let subscriptionUpdate = Promise.resolve();
|
|
127
128
|
let subscriptionPolicy = "";
|
|
129
|
+
let subscriptionFailed = false;
|
|
130
|
+
let subscriptionGeneration = 0;
|
|
131
|
+
let gapScanTimer;
|
|
132
|
+
let lastReconcileStart;
|
|
133
|
+
let gapGeneration = 0;
|
|
128
134
|
const updateSubscription = () => {
|
|
129
135
|
const update = subscriptionUpdate
|
|
130
136
|
.catch(() => { })
|
|
@@ -134,16 +140,31 @@ function startWatcher(opts) {
|
|
|
134
140
|
...(yield (0, watcher_ignore_1.readProjectWatcherIgnores)(projectRoot)),
|
|
135
141
|
];
|
|
136
142
|
const policy = JSON.stringify(ignores);
|
|
137
|
-
if (closing ||
|
|
143
|
+
if (closing ||
|
|
144
|
+
(subscription && !subscriptionFailed && subscriptionPolicy === policy))
|
|
138
145
|
return;
|
|
146
|
+
const generation = ++subscriptionGeneration;
|
|
147
|
+
subscriptionFailed = false;
|
|
139
148
|
yield subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe();
|
|
140
149
|
subscription = null;
|
|
141
150
|
if (closing)
|
|
142
151
|
return;
|
|
143
152
|
const next = yield watcher.subscribe(projectRoot, (err, events) => {
|
|
153
|
+
var _a;
|
|
154
|
+
if (closing || generation !== subscriptionGeneration)
|
|
155
|
+
return;
|
|
144
156
|
if (err) {
|
|
145
157
|
console.error(`[${wtag}] Watcher error:`, err);
|
|
146
|
-
|
|
158
|
+
scanHealthy = false;
|
|
159
|
+
(_a = opts.onHealthChange) === null || _a === void 0 ? void 0 : _a.call(opts, false, 1);
|
|
160
|
+
gapGeneration++;
|
|
161
|
+
if ((0, watcher_errors_1.isFSEventsGap)(err))
|
|
162
|
+
requestGapScan();
|
|
163
|
+
else {
|
|
164
|
+
// Terminal backend errors can clear Parcel's callbacks.
|
|
165
|
+
subscriptionFailed = true;
|
|
166
|
+
reconcile();
|
|
167
|
+
}
|
|
147
168
|
}
|
|
148
169
|
for (const event of events)
|
|
149
170
|
processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
|
|
@@ -180,6 +201,8 @@ function startWatcher(opts) {
|
|
|
180
201
|
let incompleteRetries = 0;
|
|
181
202
|
do {
|
|
182
203
|
reconcileRequested = false;
|
|
204
|
+
lastReconcileStart = Date.now();
|
|
205
|
+
const observedGapGeneration = gapGeneration;
|
|
183
206
|
filePolicy.invalidateIgnoreCache();
|
|
184
207
|
yield updateSubscription();
|
|
185
208
|
if (closing)
|
|
@@ -210,6 +233,7 @@ function startWatcher(opts) {
|
|
|
210
233
|
}
|
|
211
234
|
processor.handleFileEvent("change", absolute, {
|
|
212
235
|
forceReprocess: reconciliation.action === "reprocess",
|
|
236
|
+
workKind: "catchup",
|
|
213
237
|
});
|
|
214
238
|
}
|
|
215
239
|
}
|
|
@@ -227,6 +251,7 @@ function startWatcher(opts) {
|
|
|
227
251
|
!(0, walker_1.isPathProtectedByWalkState)(cachedPath, state)) {
|
|
228
252
|
processor.handleFileEvent("unlink", cachedPath, {
|
|
229
253
|
forceDelete: true,
|
|
254
|
+
workKind: "cleanup",
|
|
230
255
|
});
|
|
231
256
|
}
|
|
232
257
|
}
|
|
@@ -241,7 +266,7 @@ function startWatcher(opts) {
|
|
|
241
266
|
}
|
|
242
267
|
}
|
|
243
268
|
else {
|
|
244
|
-
scanHealthy =
|
|
269
|
+
scanHealthy = observedGapGeneration === gapGeneration;
|
|
245
270
|
if (ingestionDegraded && processor.progress.pendingFiles > 0) {
|
|
246
271
|
degradedRepairQueued = true;
|
|
247
272
|
}
|
|
@@ -255,6 +280,21 @@ function startWatcher(opts) {
|
|
|
255
280
|
reconciliation = null;
|
|
256
281
|
});
|
|
257
282
|
};
|
|
283
|
+
const requestGapScan = () => {
|
|
284
|
+
if (closing || gapScanTimer)
|
|
285
|
+
return;
|
|
286
|
+
const remaining = lastReconcileStart === undefined
|
|
287
|
+
? 0
|
|
288
|
+
: watcher_errors_1.FSEVENTS_GAP_SCAN_INTERVAL_MS - (Date.now() - lastReconcileStart);
|
|
289
|
+
if (reconciliation || remaining > 0) {
|
|
290
|
+
gapScanTimer = setTimeout(() => {
|
|
291
|
+
gapScanTimer = undefined;
|
|
292
|
+
reconcile();
|
|
293
|
+
}, Math.max(remaining, watcher_errors_1.FSEVENTS_GAP_SCAN_INTERVAL_MS));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
reconcile();
|
|
297
|
+
};
|
|
258
298
|
processor = new batch_processor_1.ProjectBatchProcessor(Object.assign(Object.assign({}, opts), { filePolicy, onPolicyChange: reconcile, onReindex: (files, durationMs) => {
|
|
259
299
|
var _a;
|
|
260
300
|
(_a = opts.onReindex) === null || _a === void 0 ? void 0 : _a.call(opts, files, durationMs);
|
|
@@ -282,6 +322,8 @@ function startWatcher(opts) {
|
|
|
282
322
|
},
|
|
283
323
|
close: () => __awaiter(this, void 0, void 0, function* () {
|
|
284
324
|
closing = true;
|
|
325
|
+
subscriptionGeneration++;
|
|
326
|
+
clearTimeout(gapScanTimer);
|
|
285
327
|
yield subscriptionUpdate.catch(() => { });
|
|
286
328
|
yield subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe();
|
|
287
329
|
yield reconciliation;
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
// historically did not. This formats a single machine-readable footer so an
|
|
7
7
|
// agent can decide to caveat its answer or retry once indexing settles.
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.formatWatchQueue = formatWatchQueue;
|
|
9
10
|
exports.formatIndexStateFooter = formatIndexStateFooter;
|
|
11
|
+
function formatWatchQueue(queue) {
|
|
12
|
+
const age = queue.oldestLiveEditAgeMs === null
|
|
13
|
+
? "none"
|
|
14
|
+
: `${Math.ceil(queue.oldestLiveEditAgeMs / 1000)}s`;
|
|
15
|
+
return `live=${queue.live} catchup=${queue.catchup} cleanup=${queue.cleanup} active=${queue.activeFiles} oldestLiveEdit=${age}`;
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
18
|
* One-line footer describing an in-progress index, or null when there's
|
|
12
19
|
* nothing to say (no state, or the index is settled). Suppressing the
|
|
@@ -111,6 +111,15 @@ exports.MCP_READ_OUTPUT_SCHEMAS = {
|
|
|
111
111
|
.object({
|
|
112
112
|
indexing: zod_1.z.boolean(),
|
|
113
113
|
pendingFiles: zod_1.z.number(),
|
|
114
|
+
queue: zod_1.z
|
|
115
|
+
.object({
|
|
116
|
+
live: zod_1.z.number().int().nonnegative(),
|
|
117
|
+
catchup: zod_1.z.number().int().nonnegative(),
|
|
118
|
+
cleanup: zod_1.z.number().int().nonnegative(),
|
|
119
|
+
activeFiles: zod_1.z.number().int().nonnegative(),
|
|
120
|
+
oldestLiveEditAgeMs: zod_1.z.number().nonnegative().nullable(),
|
|
121
|
+
})
|
|
122
|
+
.optional(),
|
|
114
123
|
verifying: zod_1.z.boolean().optional(),
|
|
115
124
|
failedFiles: zod_1.z.number().optional(),
|
|
116
125
|
degraded: zod_1.z.boolean().optional(),
|
package/package.json
CHANGED