grepmax 0.26.73 → 0.26.75
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 +1 -1
- package/dist/commands/watch.js +15 -4
- package/dist/lib/daemon/watcher-manager.js +209 -170
- package/dist/lib/index/watcher-recovery.js +91 -0
- package/dist/lib/index/watcher.js +97 -61
- package/dist/lib/output/index-state-footer.js +8 -0
- package/dist/lib/store/recovery-admission.js +7 -2
- package/dist/lib/utils/watcher-store.js +3 -2
- 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.75",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
package/README.md
CHANGED
|
@@ -528,7 +528,7 @@ The package includes a separate prune-only helper with pinned Lance 12 dependenc
|
|
|
528
528
|
|
|
529
529
|
`gmax recover --table /path/to/lancedb/chunks.lance --json` reads the durable recovery receipt without starting a daemon or preparing Python. `--check` samples admission without pruning. `--prune --version <current-version> --cutoff <absolute-ISO-timestamp>` explicitly requests one offline prune. A prior uncertain attempt requires `--acknowledge-uncertain <attempt-id>` after inspection. Recovery never creates/clears quarantine, stops other owners, retries automatically or enables rewriting.
|
|
530
530
|
|
|
531
|
-
Production recovery currently requires macOS, a persistent `autostart-disabled` file in the selected store home, and closed store owners. Recovery alone uses known healthy memory/kernel pressure
|
|
531
|
+
Production recovery currently requires macOS, a persistent `autostart-disabled` file in the selected store home, and closed store owners. Recovery alone uses known healthy memory/kernel pressure and the shared aggregate resource budget; normal development retains its configured guard policy. Immediately free RAM is measured for diagnostics but has no fixed minimum: macOS can reclaim cached memory, so a low free-page count alone must not prevent cleanup. The helper reserves 512 MiB of admission capacity; this is not an OS-enforced footprint cap. Recovery samples have an eight-second total ceiling, including the established five-second kernel probe allowance, with fresh sampling at deletion admission and five-second heartbeat scheduling. Unknown, stale, warning or critical measurements refuse/interrupt recovery. Pruning needs at least 1 MiB of measured metadata disk headroom, not full-table rewrite space. Initial wheel setup requires 512 MiB; an existing environment is checked against the lock offline without that download-space requirement.
|
|
532
532
|
|
|
533
533
|
The child waits for launch and deletion admission tokens, with verified helper PID/start identity recorded as the primary exclusive owner. Existing readers retain exclusion if the parent dies while the helper is alive. Current/tagged/post-cutoff metadata is verified after pruning; recovery refuses more than 256 protected versions before deletion to bound verification. Interrupted attempts require verification and explicit acknowledgement before retry. Linux/APFS fixtures exercise native retention and interruption; supported production host admission is tested separately and still runs freshly for each operation.
|
|
534
534
|
|
package/dist/commands/watch.js
CHANGED
|
@@ -365,9 +365,10 @@ exports.watch = new commander_1.Command("watch")
|
|
|
365
365
|
},
|
|
366
366
|
});
|
|
367
367
|
console.log(`[watch:${projectName}] File watcher active`);
|
|
368
|
+
(0, watcher_store_1.heartbeat)(process.pid, watcher.health);
|
|
368
369
|
// Heartbeat — update LMDB every 60s so other processes can detect liveliness
|
|
369
370
|
const heartbeatInterval = setInterval(() => {
|
|
370
|
-
(0, watcher_store_1.heartbeat)(process.pid);
|
|
371
|
+
(0, watcher_store_1.heartbeat)(process.pid, watcher.health);
|
|
371
372
|
}, IDLE_CHECK_INTERVAL_MS);
|
|
372
373
|
// Idle timeout
|
|
373
374
|
let lastActivity = Date.now();
|
|
@@ -401,8 +402,8 @@ exports.watch
|
|
|
401
402
|
.command("status")
|
|
402
403
|
.description("Show running watchers")
|
|
403
404
|
.action(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
404
|
-
var _a;
|
|
405
|
-
var
|
|
405
|
+
var _a, _b, _c, _d;
|
|
406
|
+
var _e, _f, _g;
|
|
406
407
|
const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
407
408
|
const resp = yield sendDaemonCommand({ cmd: "status" });
|
|
408
409
|
if (resp.ok) {
|
|
@@ -421,7 +422,10 @@ exports.watch
|
|
|
421
422
|
if ((_a = p.indexState) === null || _a === void 0 ? void 0 : _a.queue)
|
|
422
423
|
console.log(` queue: ${(0, index_state_footer_1.formatWatchQueue)(p.indexState.queue)}`);
|
|
423
424
|
if (p.indexState)
|
|
424
|
-
console.log(` watcher=${(
|
|
425
|
+
console.log(` watcher=${(_e = p.indexState.watcherMode) !== null && _e !== void 0 ? _e : "unknown"} overflows=${(_f = p.indexState.overflowCount) !== null && _f !== void 0 ? _f : 0} lastReconciled=${p.indexState.lastReconciledAt ? new Date(p.indexState.lastReconciledAt).toISOString() : "unknown"} catchupMs=${(_g = p.indexState.catchupMs) !== null && _g !== void 0 ? _g : "unknown"}`);
|
|
426
|
+
const recovery = (0, index_state_footer_1.formatWatcherRecovery)((_b = p.indexState) === null || _b === void 0 ? void 0 : _b.watcherRecovery);
|
|
427
|
+
if (recovery)
|
|
428
|
+
console.log(` recovery: ${recovery}`);
|
|
425
429
|
}
|
|
426
430
|
if (projects.length === 0) {
|
|
427
431
|
console.log(" (no projects)");
|
|
@@ -434,6 +438,13 @@ exports.watch
|
|
|
434
438
|
for (const w of watchers) {
|
|
435
439
|
const age = Math.floor((Date.now() - w.startTime) / 60000);
|
|
436
440
|
console.log(` - PID: ${w.pid} | Root: ${w.projectRoot} | Running: ${age}m`);
|
|
441
|
+
const recovery = (0, index_state_footer_1.formatWatcherRecovery)((_c = w.indexState) === null || _c === void 0 ? void 0 : _c.watcherRecovery);
|
|
442
|
+
if (recovery) {
|
|
443
|
+
console.log(` health sample: ${w.indexStateAt ? new Date(w.indexStateAt).toISOString() : "unknown"}`);
|
|
444
|
+
console.log(` recovery: ${recovery}`);
|
|
445
|
+
if ((_d = w.indexState) === null || _d === void 0 ? void 0 : _d.queue)
|
|
446
|
+
console.log(` queue: ${(0, index_state_footer_1.formatWatchQueue)(w.indexState.queue)}`);
|
|
447
|
+
}
|
|
437
448
|
}
|
|
438
449
|
}
|
|
439
450
|
if (!resp.ok && watchers.length === 0) {
|
|
@@ -58,6 +58,7 @@ const walker_1 = require("../index/walker");
|
|
|
58
58
|
const watcher_1 = require("../index/watcher");
|
|
59
59
|
const watcher_errors_1 = require("../index/watcher-errors");
|
|
60
60
|
const watcher_ignore_1 = require("../index/watcher-ignore");
|
|
61
|
+
const watcher_recovery_1 = require("../index/watcher-recovery");
|
|
61
62
|
const file_utils_1 = require("../utils/file-utils");
|
|
62
63
|
const logger_1 = require("../utils/logger");
|
|
63
64
|
const project_registry_1 = require("../utils/project-registry");
|
|
@@ -97,6 +98,14 @@ function abortableDelay(ms, signal) {
|
|
|
97
98
|
* thin watchProject/unwatchProject delegators over this manager.
|
|
98
99
|
*/
|
|
99
100
|
class WatcherManager {
|
|
101
|
+
recovery(root) {
|
|
102
|
+
let tracker = this.recoveryStates.get(root);
|
|
103
|
+
if (!tracker) {
|
|
104
|
+
tracker = new watcher_recovery_1.WatcherRecoveryTracker();
|
|
105
|
+
this.recoveryStates.set(root, tracker);
|
|
106
|
+
}
|
|
107
|
+
return tracker;
|
|
108
|
+
}
|
|
100
109
|
constructor(deps) {
|
|
101
110
|
this.deps = deps;
|
|
102
111
|
this.pendingOps = new Set();
|
|
@@ -117,14 +126,15 @@ class WatcherManager {
|
|
|
117
126
|
this.degradedRoots = new Set();
|
|
118
127
|
this.terminalFailures = new Map();
|
|
119
128
|
this.watchLifecycles = new Map();
|
|
129
|
+
this.recoveryStates = new Map();
|
|
120
130
|
this.subscriptionUpdates = new Map();
|
|
121
131
|
this.watcherGenerations = new Map();
|
|
122
132
|
}
|
|
123
133
|
health(root) {
|
|
124
|
-
var _a;
|
|
125
|
-
var
|
|
134
|
+
var _a, _b;
|
|
135
|
+
var _c, _d;
|
|
126
136
|
return {
|
|
127
|
-
failedFiles: (
|
|
137
|
+
failedFiles: (_c = (_a = this.terminalFailures.get(root)) === null || _a === void 0 ? void 0 : _a.size) !== null && _c !== void 0 ? _c : 0,
|
|
128
138
|
degraded: this.isRootDegraded(root),
|
|
129
139
|
watcherMode: this.pollIntervals.has(root)
|
|
130
140
|
? "polling"
|
|
@@ -135,8 +145,9 @@ class WatcherManager {
|
|
|
135
145
|
: "native",
|
|
136
146
|
catchupRunning: this.catchups.has(root),
|
|
137
147
|
lastReconciledAt: this.reconciledAt.get(root),
|
|
138
|
-
overflowCount: (
|
|
148
|
+
overflowCount: (_d = this.overflowCounts.get(root)) !== null && _d !== void 0 ? _d : 0,
|
|
139
149
|
catchupMs: this.catchupDurations.get(root),
|
|
150
|
+
watcherRecovery: (_b = this.recoveryStates.get(root)) === null || _b === void 0 ? void 0 : _b.snapshot(),
|
|
140
151
|
};
|
|
141
152
|
}
|
|
142
153
|
watchProject(root_1) {
|
|
@@ -325,8 +336,9 @@ class WatcherManager {
|
|
|
325
336
|
}
|
|
326
337
|
catch (_c) { }
|
|
327
338
|
this.deps.subscriptions.delete(root);
|
|
339
|
+
this.recovery(root).detached();
|
|
328
340
|
}
|
|
329
|
-
const { subscription: sub } = yield (0, watcher_ignore_1.subscribeWithNativeExclusions)(root, (err, events) => {
|
|
341
|
+
const { subscription: sub, policy } = yield (0, watcher_ignore_1.subscribeWithNativeExclusions)(root, (err, events) => {
|
|
330
342
|
var _a, _b;
|
|
331
343
|
var _c, _d;
|
|
332
344
|
if (this.deps.processors.get(root) !== processor ||
|
|
@@ -334,6 +346,7 @@ class WatcherManager {
|
|
|
334
346
|
((_b = (_a = this.deps).getShuttingDown) === null || _b === void 0 ? void 0 : _b.call(_a)))
|
|
335
347
|
return;
|
|
336
348
|
if (err) {
|
|
349
|
+
this.recovery(root).error(err);
|
|
337
350
|
this.overflowCounts.set(root, ((_c = this.overflowCounts.get(root)) !== null && _c !== void 0 ? _c : 0) + 1);
|
|
338
351
|
const now = Date.now();
|
|
339
352
|
if (!this.lastWatcherErrorLogMs.has(root) ||
|
|
@@ -366,6 +379,7 @@ class WatcherManager {
|
|
|
366
379
|
return;
|
|
367
380
|
}
|
|
368
381
|
this.deps.subscriptions.set(root, sub);
|
|
382
|
+
this.recovery(root).attached(policy);
|
|
369
383
|
});
|
|
370
384
|
}
|
|
371
385
|
requestOverflowCatchup(root, processor) {
|
|
@@ -409,6 +423,7 @@ class WatcherManager {
|
|
|
409
423
|
if (sub) {
|
|
410
424
|
sub.unsubscribe().catch(() => { });
|
|
411
425
|
this.deps.subscriptions.delete(root);
|
|
426
|
+
this.recovery(root).detached();
|
|
412
427
|
}
|
|
413
428
|
if (!this.pollIntervals.has(root)) {
|
|
414
429
|
console.error(`[daemon:${name}] Native watcher failed after ${fails} terminal errors — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
|
|
@@ -583,186 +598,199 @@ class WatcherManager {
|
|
|
583
598
|
var _a, e_1, _b, _c;
|
|
584
599
|
var _d;
|
|
585
600
|
var _e, _f, _g;
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
const metaCache = this.deps.getMetaCache();
|
|
590
|
-
const vectorDb = this.deps.getVectorDb();
|
|
591
|
-
processor.filePolicy.invalidateIgnoreCache();
|
|
592
|
-
const rootPrefix = root.endsWith("/") ? root : `${root}/`;
|
|
593
|
-
const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
|
|
594
|
-
const vectorPaths = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
|
|
595
|
-
const knownPaths = new Set([...cachedPaths, ...vectorPaths]);
|
|
596
|
-
if (signal.aborted)
|
|
597
|
-
return false;
|
|
598
|
-
const seenPaths = new Set();
|
|
599
|
-
const walkState = (0, walker_1.createWalkState)();
|
|
600
|
-
let queued = 0;
|
|
601
|
-
let skipped = 0;
|
|
602
|
-
let debugSamples = 0;
|
|
601
|
+
const tracker = this.recovery(root);
|
|
602
|
+
const scan = tracker.beginScan();
|
|
603
|
+
let outcome = "failed";
|
|
603
604
|
try {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
(0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
|
|
642
|
-
yield abortableDelay(5000, signal);
|
|
605
|
+
const scanStart = Date.now();
|
|
606
|
+
const overflowCountAtStart = (_e = this.overflowCounts.get(root)) !== null && _e !== void 0 ? _e : 0;
|
|
607
|
+
const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
|
|
608
|
+
const metaCache = this.deps.getMetaCache();
|
|
609
|
+
const vectorDb = this.deps.getVectorDb();
|
|
610
|
+
processor.filePolicy.invalidateIgnoreCache();
|
|
611
|
+
const rootPrefix = root.endsWith("/") ? root : `${root}/`;
|
|
612
|
+
const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
|
|
613
|
+
const vectorPaths = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
|
|
614
|
+
const knownPaths = new Set([...cachedPaths, ...vectorPaths]);
|
|
615
|
+
if (signal.aborted)
|
|
616
|
+
return false;
|
|
617
|
+
const seenPaths = new Set();
|
|
618
|
+
const walkState = (0, walker_1.createWalkState)();
|
|
619
|
+
let queued = 0;
|
|
620
|
+
let skipped = 0;
|
|
621
|
+
let debugSamples = 0;
|
|
622
|
+
try {
|
|
623
|
+
for (var _h = true, _j = __asyncValues((0, walker_1.walk)(root, {
|
|
624
|
+
policy: processor.filePolicy,
|
|
625
|
+
state: walkState,
|
|
626
|
+
})), _k; _k = yield _j.next(), _a = _k.done, !_a; _h = true) {
|
|
627
|
+
_c = _k.value;
|
|
628
|
+
_h = false;
|
|
629
|
+
const relPath = _c;
|
|
630
|
+
if (signal.aborted)
|
|
631
|
+
return false;
|
|
632
|
+
const absPath = path.join(root, relPath);
|
|
633
|
+
try {
|
|
634
|
+
const classification = yield processor.filePolicy.classifyFile(absPath);
|
|
635
|
+
if (classification.status === "error") {
|
|
636
|
+
walkState.protectedPaths.add(classification.protectedPath);
|
|
637
|
+
walkState.errors.push({
|
|
638
|
+
path: classification.protectedPath,
|
|
639
|
+
error: classification.error,
|
|
640
|
+
});
|
|
641
|
+
continue;
|
|
643
642
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
643
|
+
if (classification.status !== "indexable")
|
|
644
|
+
continue;
|
|
645
|
+
const stats = classification.stat;
|
|
646
|
+
seenPaths.add(absPath);
|
|
647
|
+
let cached = metaCache.get(absPath);
|
|
648
|
+
const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(absPath, cached, vectorPaths.has(absPath));
|
|
649
|
+
if (reconciliation.action === "stamp") {
|
|
650
|
+
cached = reconciliation.entry;
|
|
651
|
+
metaCache.put(absPath, cached);
|
|
652
|
+
}
|
|
653
|
+
else if (reconciliation.action === "reprocess") {
|
|
654
|
+
processor.handleFileEvent("change", absPath, {
|
|
655
|
+
forceReprocess: true,
|
|
656
|
+
workKind: "catchup",
|
|
653
657
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
});
|
|
659
|
-
continue;
|
|
658
|
+
queued++;
|
|
659
|
+
if (queued % 500 === 0) {
|
|
660
|
+
(0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
|
|
661
|
+
yield abortableDelay(5000, signal);
|
|
660
662
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (!isFileCached(cached, stats)) {
|
|
666
|
+
// Fast path: if only mtime changed but size is identical and we have a hash,
|
|
667
|
+
// just verify the hash in-process instead of sending to a worker.
|
|
668
|
+
if ((cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
|
|
669
|
+
const observed = Object.assign({}, cached);
|
|
670
|
+
const snapshot = yield (0, file_utils_1.readFileSnapshot)(absPath, {
|
|
671
|
+
projectRoot: root,
|
|
672
|
+
});
|
|
673
|
+
if (snapshot.size !== stats.size ||
|
|
674
|
+
snapshot.mtimeMs !== stats.mtimeMs) {
|
|
675
|
+
processor.handleFileEvent("change", absPath, {
|
|
676
|
+
workKind: "catchup",
|
|
677
|
+
});
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const hash = (0, file_utils_1.computeContentHash)(snapshot.buffer, absPath);
|
|
681
|
+
if (hash === observed.hash) {
|
|
682
|
+
// Content unchanged — update mtime in cache and skip worker
|
|
683
|
+
if (signal.aborted)
|
|
684
|
+
return false;
|
|
685
|
+
// A live batch can finish while this read yields. Never replace
|
|
686
|
+
// newer metadata or resurrect a cache entry it removed.
|
|
687
|
+
const current = metaCache.get(absPath);
|
|
688
|
+
if (current &&
|
|
689
|
+
current.hash === observed.hash &&
|
|
690
|
+
current.mtimeMs === observed.mtimeMs &&
|
|
691
|
+
current.size === observed.size &&
|
|
692
|
+
current.hashVersion === observed.hashVersion &&
|
|
693
|
+
current.hasVectors === observed.hasVectors) {
|
|
694
|
+
metaCache.put(absPath, Object.assign(Object.assign({}, observed), { mtimeMs: stats.mtimeMs }));
|
|
695
|
+
}
|
|
696
|
+
skipped++;
|
|
697
|
+
continue;
|
|
676
698
|
}
|
|
677
|
-
|
|
678
|
-
|
|
699
|
+
}
|
|
700
|
+
// Debug: log first few misses to diagnose re-queue loops
|
|
701
|
+
if (debugSamples < 5) {
|
|
702
|
+
(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}`);
|
|
703
|
+
debugSamples++;
|
|
704
|
+
}
|
|
705
|
+
if (signal.aborted)
|
|
706
|
+
return false;
|
|
707
|
+
processor.handleFileEvent("change", absPath, {
|
|
708
|
+
workKind: "catchup",
|
|
709
|
+
});
|
|
710
|
+
queued++;
|
|
711
|
+
// Throttle: pause periodically during large catchup scans to let the
|
|
712
|
+
// batch processor drain and compaction run between bursts.
|
|
713
|
+
if (queued % 500 === 0) {
|
|
714
|
+
(0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
|
|
715
|
+
yield abortableDelay(5000, signal);
|
|
679
716
|
}
|
|
680
717
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
(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}`);
|
|
684
|
-
debugSamples++;
|
|
685
|
-
}
|
|
686
|
-
if (signal.aborted)
|
|
687
|
-
return false;
|
|
688
|
-
processor.handleFileEvent("change", absPath, { workKind: "catchup" });
|
|
689
|
-
queued++;
|
|
690
|
-
// Throttle: pause periodically during large catchup scans to let the
|
|
691
|
-
// batch processor drain and compaction run between bursts.
|
|
692
|
-
if (queued % 500 === 0) {
|
|
693
|
-
(0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
|
|
694
|
-
yield abortableDelay(5000, signal);
|
|
718
|
+
else {
|
|
719
|
+
skipped++;
|
|
695
720
|
}
|
|
696
721
|
}
|
|
697
|
-
|
|
698
|
-
|
|
722
|
+
catch (error) {
|
|
723
|
+
walkState.protectedPaths.add(absPath);
|
|
724
|
+
walkState.errors.push({ path: absPath, error });
|
|
699
725
|
}
|
|
700
726
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
727
|
+
}
|
|
728
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
729
|
+
finally {
|
|
730
|
+
try {
|
|
731
|
+
if (!_h && !_a && (_b = _j.return)) yield _b.call(_j);
|
|
704
732
|
}
|
|
733
|
+
finally { if (e_1) throw e_1.error; }
|
|
705
734
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
735
|
+
(0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
|
|
736
|
+
// Purge files deleted while daemon was offline
|
|
737
|
+
let purged = 0;
|
|
738
|
+
for (const cachedPath of knownPaths) {
|
|
739
|
+
if (signal.aborted)
|
|
740
|
+
return false;
|
|
741
|
+
if (!seenPaths.has(cachedPath) &&
|
|
742
|
+
!(0, walker_1.isPathProtectedByWalkState)(cachedPath, walkState)) {
|
|
743
|
+
processor.handleFileEvent("unlink", cachedPath, {
|
|
744
|
+
forceDelete: true,
|
|
745
|
+
workKind: "cleanup",
|
|
746
|
+
});
|
|
747
|
+
purged++;
|
|
748
|
+
}
|
|
711
749
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
processor.handleFileEvent("unlink", cachedPath, {
|
|
723
|
-
forceDelete: true,
|
|
724
|
-
workKind: "cleanup",
|
|
750
|
+
const complete = walkState.rootComplete && walkState.errors.length === 0;
|
|
751
|
+
if (!complete) {
|
|
752
|
+
this.degradedRoots.add(root);
|
|
753
|
+
(0, watcher_store_1.registerWatcher)({
|
|
754
|
+
pid: process.pid,
|
|
755
|
+
projectRoot: root,
|
|
756
|
+
startTime: Date.now(),
|
|
757
|
+
status: "degraded",
|
|
758
|
+
lastHeartbeat: Date.now(),
|
|
759
|
+
lastError: `${walkState.errors.length} scan path(s) incomplete`,
|
|
725
760
|
});
|
|
726
|
-
purged++;
|
|
727
761
|
}
|
|
762
|
+
else {
|
|
763
|
+
this.degradedRoots.delete(root);
|
|
764
|
+
const failedFiles = (_f = (_d = this.terminalFailures.get(root)) === null || _d === void 0 ? void 0 : _d.size) !== null && _f !== void 0 ? _f : 0;
|
|
765
|
+
(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
|
|
766
|
+
? {
|
|
767
|
+
lastError: `${failedFiles} file(s) exhausted automatic retries`,
|
|
768
|
+
}
|
|
769
|
+
: {})));
|
|
770
|
+
}
|
|
771
|
+
if (queued > 0 || purged > 0) {
|
|
772
|
+
const parts = [];
|
|
773
|
+
if (queued > 0)
|
|
774
|
+
parts.push(`${queued} changed`);
|
|
775
|
+
if (purged > 0)
|
|
776
|
+
parts.push(`${purged} deleted`);
|
|
777
|
+
console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
|
|
778
|
+
}
|
|
779
|
+
const ended = Date.now();
|
|
780
|
+
this.lastCatchupEndMs.set(root, ended);
|
|
781
|
+
this.catchupDurations.set(root, ended - scanStart);
|
|
782
|
+
if (complete) {
|
|
783
|
+
this.reconciledAt.set(root, ended);
|
|
784
|
+
// A later gap still needs its deferred scan, even if this walk succeeded.
|
|
785
|
+
if (((_g = this.overflowCounts.get(root)) !== null && _g !== void 0 ? _g : 0) === overflowCountAtStart)
|
|
786
|
+
this.overflowPendingRoots.delete(root);
|
|
787
|
+
}
|
|
788
|
+
outcome = complete ? "complete" : "incomplete";
|
|
789
|
+
return complete;
|
|
728
790
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
this.degradedRoots.add(root);
|
|
732
|
-
(0, watcher_store_1.registerWatcher)({
|
|
733
|
-
pid: process.pid,
|
|
734
|
-
projectRoot: root,
|
|
735
|
-
startTime: Date.now(),
|
|
736
|
-
status: "degraded",
|
|
737
|
-
lastHeartbeat: Date.now(),
|
|
738
|
-
lastError: `${walkState.errors.length} scan path(s) incomplete`,
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
else {
|
|
742
|
-
this.degradedRoots.delete(root);
|
|
743
|
-
const failedFiles = (_f = (_d = this.terminalFailures.get(root)) === null || _d === void 0 ? void 0 : _d.size) !== null && _f !== void 0 ? _f : 0;
|
|
744
|
-
(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
|
|
745
|
-
? { lastError: `${failedFiles} file(s) exhausted automatic retries` }
|
|
746
|
-
: {})));
|
|
747
|
-
}
|
|
748
|
-
if (queued > 0 || purged > 0) {
|
|
749
|
-
const parts = [];
|
|
750
|
-
if (queued > 0)
|
|
751
|
-
parts.push(`${queued} changed`);
|
|
752
|
-
if (purged > 0)
|
|
753
|
-
parts.push(`${purged} deleted`);
|
|
754
|
-
console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
|
|
755
|
-
}
|
|
756
|
-
const ended = Date.now();
|
|
757
|
-
this.lastCatchupEndMs.set(root, ended);
|
|
758
|
-
this.catchupDurations.set(root, ended - scanStart);
|
|
759
|
-
if (complete) {
|
|
760
|
-
this.reconciledAt.set(root, ended);
|
|
761
|
-
// A later gap still needs its deferred scan, even if this walk succeeded.
|
|
762
|
-
if (((_g = this.overflowCounts.get(root)) !== null && _g !== void 0 ? _g : 0) === overflowCountAtStart)
|
|
763
|
-
this.overflowPendingRoots.delete(root);
|
|
791
|
+
finally {
|
|
792
|
+
tracker.finishScan(scan, signal.aborted ? "aborted" : outcome);
|
|
764
793
|
}
|
|
765
|
-
return complete;
|
|
766
794
|
});
|
|
767
795
|
}
|
|
768
796
|
runCatchup(root, processor) {
|
|
@@ -792,9 +820,19 @@ class WatcherManager {
|
|
|
792
820
|
}
|
|
793
821
|
} while (active.dirty && !signal.aborted);
|
|
794
822
|
});
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
823
|
+
const tracker = this.recovery(root);
|
|
824
|
+
const scansBeforeAttempt = tracker.snapshot().scanCount;
|
|
825
|
+
const run = (() => __awaiter(this, void 0, void 0, function* () {
|
|
826
|
+
try {
|
|
827
|
+
return yield (this.deps.runProjectOperation
|
|
828
|
+
? this.deps.runProjectOperation(root, "watch-catchup", active.controller.signal, execute)
|
|
829
|
+
: execute(active.controller.signal));
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
tracker.attemptFailed(tracker.snapshot().scanCount > scansBeforeAttempt, active.controller.signal.aborted);
|
|
833
|
+
throw error;
|
|
834
|
+
}
|
|
835
|
+
}))().finally(() => {
|
|
798
836
|
if (this.catchups.get(root) === active)
|
|
799
837
|
this.catchups.delete(root);
|
|
800
838
|
});
|
|
@@ -855,6 +893,7 @@ class WatcherManager {
|
|
|
855
893
|
this.overflowPendingRoots.delete(root);
|
|
856
894
|
this.degradedRoots.delete(root);
|
|
857
895
|
this.terminalFailures.delete(root);
|
|
896
|
+
this.recoveryStates.delete(root);
|
|
858
897
|
(0, watcher_store_1.unregisterWatcherByRoot)(root);
|
|
859
898
|
console.log(`[daemon] Unwatched ${root}`);
|
|
860
899
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WatcherRecoveryTracker = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const watcher_errors_1 = require("./watcher-errors");
|
|
6
|
+
/** Fixed-size, process/lifecycle-local evidence. A completed walk covers only
|
|
7
|
+
* errors already observed when it began; queued ingestion is reported separately. */
|
|
8
|
+
class WatcherRecoveryTracker {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.gapCount = 0;
|
|
11
|
+
this.terminalErrorCount = 0;
|
|
12
|
+
this.coveredGapCount = 0;
|
|
13
|
+
this.coveredTerminalErrorCount = 0;
|
|
14
|
+
this.scanCount = 0;
|
|
15
|
+
this.scanIncomplete = false;
|
|
16
|
+
}
|
|
17
|
+
error(error) {
|
|
18
|
+
if ((0, watcher_errors_1.isFSEventsGap)(error)) {
|
|
19
|
+
this.gapCount++;
|
|
20
|
+
this.lastGapAt = Date.now();
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
this.terminalErrorCount++;
|
|
24
|
+
this.lastTerminalErrorAt = Date.now();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
beginScan() {
|
|
28
|
+
const scan = {
|
|
29
|
+
id: ++this.scanCount,
|
|
30
|
+
startedAt: Date.now(),
|
|
31
|
+
gapCountAtStart: this.gapCount,
|
|
32
|
+
terminalErrorCountAtStart: this.terminalErrorCount,
|
|
33
|
+
outcome: "running",
|
|
34
|
+
};
|
|
35
|
+
this.lastScan = scan;
|
|
36
|
+
return scan;
|
|
37
|
+
}
|
|
38
|
+
finishScan(scan, outcome) {
|
|
39
|
+
scan.finishedAt = Date.now();
|
|
40
|
+
scan.outcome = outcome;
|
|
41
|
+
if (outcome === "complete") {
|
|
42
|
+
this.scanIncomplete = false;
|
|
43
|
+
this.coveredGapCount = Math.max(this.coveredGapCount, scan.gapCountAtStart);
|
|
44
|
+
this.coveredTerminalErrorCount = Math.max(this.coveredTerminalErrorCount, scan.terminalErrorCountAtStart);
|
|
45
|
+
this.lastCompleteScan = Object.assign({}, scan);
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
this.scanIncomplete = true;
|
|
49
|
+
}
|
|
50
|
+
attemptFailed(scanStarted, aborted) {
|
|
51
|
+
this.scanIncomplete = true;
|
|
52
|
+
this.lastAttemptFailure = { at: Date.now(), scanStarted, aborted };
|
|
53
|
+
}
|
|
54
|
+
attached(policy) {
|
|
55
|
+
// The subscription helper returns the exact accepted policy, including its
|
|
56
|
+
// fallback. Summarize that value; never rediscover directories for health.
|
|
57
|
+
const patterns = JSON.parse(policy);
|
|
58
|
+
const literalCount = patterns.filter((pattern) => !pattern.startsWith("/") && !pattern.includes("*")).length;
|
|
59
|
+
this.exclusions = {
|
|
60
|
+
attached: true,
|
|
61
|
+
literalCount,
|
|
62
|
+
filterCount: patterns.length - literalCount,
|
|
63
|
+
fingerprint: (0, node_crypto_1.createHash)("sha256").update(policy).digest("hex"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
detached() {
|
|
67
|
+
if (this.exclusions)
|
|
68
|
+
this.exclusions.attached = false;
|
|
69
|
+
}
|
|
70
|
+
snapshot() {
|
|
71
|
+
var _a;
|
|
72
|
+
return {
|
|
73
|
+
gapCount: this.gapCount,
|
|
74
|
+
terminalErrorCount: this.terminalErrorCount,
|
|
75
|
+
coveredGapCount: this.coveredGapCount,
|
|
76
|
+
outstandingGapCount: this.gapCount - this.coveredGapCount,
|
|
77
|
+
reconciliationNeeded: this.scanIncomplete ||
|
|
78
|
+
((_a = this.lastScan) === null || _a === void 0 ? void 0 : _a.outcome) === "running" ||
|
|
79
|
+
this.gapCount > this.coveredGapCount ||
|
|
80
|
+
this.terminalErrorCount > this.coveredTerminalErrorCount,
|
|
81
|
+
lastGapAt: this.lastGapAt,
|
|
82
|
+
lastTerminalErrorAt: this.lastTerminalErrorAt,
|
|
83
|
+
scanCount: this.scanCount,
|
|
84
|
+
lastScan: this.lastScan && Object.assign({}, this.lastScan),
|
|
85
|
+
lastCompleteScan: this.lastCompleteScan && Object.assign({}, this.lastCompleteScan),
|
|
86
|
+
lastAttemptFailure: this.lastAttemptFailure && Object.assign({}, this.lastAttemptFailure),
|
|
87
|
+
exclusions: this.exclusions && Object.assign({}, this.exclusions),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.WatcherRecoveryTracker = WatcherRecoveryTracker;
|
|
@@ -59,6 +59,7 @@ const ignore_patterns_1 = require("./ignore-patterns");
|
|
|
59
59
|
const walker_1 = require("./walker");
|
|
60
60
|
const watcher_errors_1 = require("./watcher-errors");
|
|
61
61
|
const watcher_ignore_1 = require("./watcher-ignore");
|
|
62
|
+
const watcher_recovery_1 = require("./watcher-recovery");
|
|
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
|
|
64
65
|
// "dist" only covered <root>/dist — nested build dirs (packages/*/dist,
|
|
@@ -114,6 +115,7 @@ function startWatcher(opts) {
|
|
|
114
115
|
const { projectRoot } = opts;
|
|
115
116
|
const wtag = `watch:${projectRoot.split("/").pop()}`;
|
|
116
117
|
const filePolicy = new file_policy_1.ProjectFilePolicy(projectRoot);
|
|
118
|
+
const recovery = new watcher_recovery_1.WatcherRecoveryTracker();
|
|
117
119
|
let reconciliation = null;
|
|
118
120
|
let reconcileRequested = false;
|
|
119
121
|
let closing = false;
|
|
@@ -143,6 +145,7 @@ function startWatcher(opts) {
|
|
|
143
145
|
subscriptionFailed = false;
|
|
144
146
|
yield subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe();
|
|
145
147
|
subscription = null;
|
|
148
|
+
recovery.detached();
|
|
146
149
|
if (closing)
|
|
147
150
|
return;
|
|
148
151
|
const { subscription: next, policy: attachedPolicy } = yield (0, watcher_ignore_1.subscribeWithNativeExclusions)(projectRoot, (err, events) => {
|
|
@@ -150,6 +153,7 @@ function startWatcher(opts) {
|
|
|
150
153
|
if (closing || generation !== subscriptionGeneration)
|
|
151
154
|
return;
|
|
152
155
|
if (err) {
|
|
156
|
+
recovery.error(err);
|
|
153
157
|
console.error(`[${wtag}] Watcher error:`, err);
|
|
154
158
|
scanHealthy = false;
|
|
155
159
|
(_a = opts.onHealthChange) === null || _a === void 0 ? void 0 : _a.call(opts, false, 1);
|
|
@@ -171,6 +175,7 @@ function startWatcher(opts) {
|
|
|
171
175
|
}
|
|
172
176
|
subscription = next;
|
|
173
177
|
subscriptionPolicy = attachedPolicy;
|
|
178
|
+
recovery.attached(attachedPolicy);
|
|
174
179
|
}));
|
|
175
180
|
subscriptionUpdate = update;
|
|
176
181
|
return update;
|
|
@@ -199,76 +204,88 @@ function startWatcher(opts) {
|
|
|
199
204
|
reconcileRequested = false;
|
|
200
205
|
lastReconcileStart = Date.now();
|
|
201
206
|
const observedGapGeneration = gapGeneration;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return;
|
|
206
|
-
const rootPrefix = projectRoot.endsWith("/")
|
|
207
|
-
? projectRoot
|
|
208
|
-
: `${projectRoot}/`;
|
|
209
|
-
const cached = yield opts.metaCache.getKeysWithPrefix(rootPrefix);
|
|
210
|
-
const vectorPaths = yield opts.vectorDb.getDistinctPathsForPrefix(rootPrefix);
|
|
211
|
-
const knownPaths = new Set([...cached, ...vectorPaths]);
|
|
212
|
-
const seen = new Set();
|
|
213
|
-
const state = (0, walker_1.createWalkState)();
|
|
207
|
+
const scan = recovery.beginScan();
|
|
208
|
+
let outcome = "failed";
|
|
209
|
+
let retryDelayMs = 0;
|
|
214
210
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
211
|
+
filePolicy.invalidateIgnoreCache();
|
|
212
|
+
yield updateSubscription();
|
|
213
|
+
if (closing)
|
|
214
|
+
return;
|
|
215
|
+
const rootPrefix = projectRoot.endsWith("/")
|
|
216
|
+
? projectRoot
|
|
217
|
+
: `${projectRoot}/`;
|
|
218
|
+
const cached = yield opts.metaCache.getKeysWithPrefix(rootPrefix);
|
|
219
|
+
const vectorPaths = yield opts.vectorDb.getDistinctPathsForPrefix(rootPrefix);
|
|
220
|
+
const knownPaths = new Set([...cached, ...vectorPaths]);
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
const state = (0, walker_1.createWalkState)();
|
|
223
|
+
try {
|
|
224
|
+
for (var _e = true, _f = (e_1 = void 0, __asyncValues((0, walker_1.walk)(projectRoot, {
|
|
225
|
+
policy: filePolicy,
|
|
226
|
+
state,
|
|
227
|
+
}))), _g; _g = yield _f.next(), _a = _g.done, !_a; _e = true) {
|
|
228
|
+
_c = _g.value;
|
|
229
|
+
_e = false;
|
|
230
|
+
const relative = _c;
|
|
231
|
+
if (closing)
|
|
232
|
+
return;
|
|
233
|
+
const absolute = path.join(projectRoot, relative);
|
|
234
|
+
seen.add(absolute);
|
|
235
|
+
const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(absolute, opts.metaCache.get(absolute), vectorPaths.has(absolute));
|
|
236
|
+
if (reconciliation.action === "stamp") {
|
|
237
|
+
opts.metaCache.put(absolute, reconciliation.entry);
|
|
238
|
+
}
|
|
239
|
+
processor.handleFileEvent("change", absolute, {
|
|
240
|
+
forceReprocess: reconciliation.action === "reprocess",
|
|
241
|
+
workKind: "catchup",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
246
|
+
finally {
|
|
247
|
+
try {
|
|
248
|
+
if (!_e && !_a && (_b = _f.return)) yield _b.call(_f);
|
|
249
|
+
}
|
|
250
|
+
finally { if (e_1) throw e_1.error; }
|
|
251
|
+
}
|
|
252
|
+
for (const cachedPath of knownPaths) {
|
|
222
253
|
if (closing)
|
|
223
254
|
return;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
255
|
+
if (!seen.has(cachedPath) &&
|
|
256
|
+
!(0, walker_1.isPathProtectedByWalkState)(cachedPath, state)) {
|
|
257
|
+
processor.handleFileEvent("unlink", cachedPath, {
|
|
258
|
+
forceDelete: true,
|
|
259
|
+
workKind: "cleanup",
|
|
260
|
+
});
|
|
229
261
|
}
|
|
230
|
-
processor.handleFileEvent("change", absolute, {
|
|
231
|
-
forceReprocess: reconciliation.action === "reprocess",
|
|
232
|
-
workKind: "catchup",
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
237
|
-
finally {
|
|
238
|
-
try {
|
|
239
|
-
if (!_e && !_a && (_b = _f.return)) yield _b.call(_f);
|
|
240
262
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
263
|
+
if (!state.rootComplete || state.errors.length > 0) {
|
|
264
|
+
outcome = "incomplete";
|
|
265
|
+
scanHealthy = false;
|
|
266
|
+
(_d = opts.onHealthChange) === null || _d === void 0 ? void 0 : _d.call(opts, false, state.errors.length);
|
|
267
|
+
console.error(`[${wtag}] Reconciliation incomplete at ${state.errors.length} path(s)`);
|
|
268
|
+
if (incompleteRetries < 3) {
|
|
269
|
+
incompleteRetries++;
|
|
270
|
+
reconcileRequested = true;
|
|
271
|
+
retryDelayMs = 1000 * Math.pow(2, (incompleteRetries - 1));
|
|
272
|
+
}
|
|
252
273
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
yield new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, (incompleteRetries - 1))));
|
|
274
|
+
else {
|
|
275
|
+
outcome = "complete";
|
|
276
|
+
scanHealthy = observedGapGeneration === gapGeneration;
|
|
277
|
+
if (ingestionDegraded && processor.progress.pendingFiles > 0) {
|
|
278
|
+
degradedRepairQueued = true;
|
|
279
|
+
}
|
|
280
|
+
reportHealthyIfSettled();
|
|
281
|
+
incompleteRetries = 0;
|
|
262
282
|
}
|
|
263
283
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (ingestionDegraded && processor.progress.pendingFiles > 0) {
|
|
267
|
-
degradedRepairQueued = true;
|
|
268
|
-
}
|
|
269
|
-
reportHealthyIfSettled();
|
|
270
|
-
incompleteRetries = 0;
|
|
284
|
+
finally {
|
|
285
|
+
recovery.finishScan(scan, closing ? "aborted" : outcome);
|
|
271
286
|
}
|
|
287
|
+
if (retryDelayMs > 0)
|
|
288
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
272
289
|
} while (reconcileRequested && !closing);
|
|
273
290
|
}))()
|
|
274
291
|
.catch((err) => console.error(`[${wtag}] Reconciliation failed:`, err))
|
|
@@ -313,6 +330,24 @@ function startWatcher(opts) {
|
|
|
313
330
|
yield updateSubscription();
|
|
314
331
|
reconcile();
|
|
315
332
|
return {
|
|
333
|
+
get health() {
|
|
334
|
+
const progress = processor.progress;
|
|
335
|
+
const state = recovery.snapshot();
|
|
336
|
+
return {
|
|
337
|
+
indexing: progress.processing || progress.pendingFiles > 0,
|
|
338
|
+
pendingFiles: progress.pendingFiles,
|
|
339
|
+
queue: progress.queue,
|
|
340
|
+
failedFiles: Math.max(terminalFailures.size, progress.failedFiles),
|
|
341
|
+
degraded: !scanHealthy || ingestionDegraded || terminalFailures.size > 0,
|
|
342
|
+
watcherMode: closing
|
|
343
|
+
? undefined
|
|
344
|
+
: subscriptionFailed || gapScanTimer || state.reconciliationNeeded
|
|
345
|
+
? "recovering"
|
|
346
|
+
: "native",
|
|
347
|
+
catchupRunning: reconciliation !== null,
|
|
348
|
+
watcherRecovery: state,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
316
351
|
get progress() {
|
|
317
352
|
return processor.progress;
|
|
318
353
|
},
|
|
@@ -322,6 +357,7 @@ function startWatcher(opts) {
|
|
|
322
357
|
clearTimeout(gapScanTimer);
|
|
323
358
|
yield subscriptionUpdate.catch(() => { });
|
|
324
359
|
yield subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe();
|
|
360
|
+
recovery.detached();
|
|
325
361
|
yield reconciliation;
|
|
326
362
|
yield processor.close();
|
|
327
363
|
}),
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// agent can decide to caveat its answer or retry once indexing settles.
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.formatWatchQueue = formatWatchQueue;
|
|
10
|
+
exports.formatWatcherRecovery = formatWatcherRecovery;
|
|
10
11
|
exports.formatIndexStateFooter = formatIndexStateFooter;
|
|
11
12
|
function formatWatchQueue(queue) {
|
|
12
13
|
const age = queue.oldestLiveEditAgeMs === null
|
|
@@ -14,6 +15,13 @@ function formatWatchQueue(queue) {
|
|
|
14
15
|
: `${Math.ceil(queue.oldestLiveEditAgeMs / 1000)}s`;
|
|
15
16
|
return `live=${queue.live} catchup=${queue.catchup} cleanup=${queue.cleanup} active=${queue.activeFiles} oldestLiveEdit=${age}`;
|
|
16
17
|
}
|
|
18
|
+
function formatWatcherRecovery(state) {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
var _c, _d;
|
|
21
|
+
if (!state)
|
|
22
|
+
return null;
|
|
23
|
+
return `gaps=${state.gapCount} covered=${state.coveredGapCount} outstanding=${state.outstandingGapCount} terminalErrors=${state.terminalErrorCount} reconciliationNeeded=${state.reconciliationNeeded} scan=${(_c = (_a = state.lastScan) === null || _a === void 0 ? void 0 : _a.id) !== null && _c !== void 0 ? _c : "none"}/${(_d = (_b = state.lastScan) === null || _b === void 0 ? void 0 : _b.outcome) !== null && _d !== void 0 ? _d : "none"} exclusions=${state.exclusions ? `${state.exclusions.attached ? "attached" : "detached"}:${state.exclusions.literalCount}+${state.exclusions.filterCount}:${state.exclusions.fingerprint}` : "unknown"}`;
|
|
24
|
+
}
|
|
17
25
|
/**
|
|
18
26
|
* One-line footer describing an in-progress index, or null when there's
|
|
19
27
|
* nothing to say (no state, or the index is settled). Suppressing the
|
|
@@ -65,10 +65,15 @@ function assertRecoveryAdmission(storeDir, budget) {
|
|
|
65
65
|
const snapshot = budget.check();
|
|
66
66
|
if ((snapshot === null || snapshot === void 0 ? void 0 : snapshot.platform) !== "darwin")
|
|
67
67
|
throw new Error("Production recovery admission currently requires macOS host measurements");
|
|
68
|
+
// os.freemem() reports immediately free pages, not all memory macOS can
|
|
69
|
+
// reclaim. Requiring an idle 512MiB here refused cleanup on a healthy host
|
|
70
|
+
// as the file cache changed. Strict pressure/aggregate checks above and the
|
|
71
|
+
// shared 512MiB reservation below govern helper admission; free pages remain
|
|
72
|
+
// measured diagnostics, never a substitute for those checks.
|
|
68
73
|
if (snapshot.physicalFreeMb === null ||
|
|
69
74
|
!Number.isFinite(snapshot.physicalFreeMb) ||
|
|
70
|
-
snapshot.physicalFreeMb <
|
|
71
|
-
throw new Error("
|
|
75
|
+
snapshot.physicalFreeMb < 0)
|
|
76
|
+
throw new Error("Measured physical free memory is unavailable");
|
|
72
77
|
return snapshot;
|
|
73
78
|
}
|
|
74
79
|
function admitPrune(storeDir, budget = createRecoveryBudget()) {
|
|
@@ -109,11 +109,12 @@ function updateWatcherStatus(pid, status, lastReindex, lastError) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
function heartbeat(pid) {
|
|
112
|
+
function heartbeat(pid, indexState) {
|
|
113
113
|
const db = getDb();
|
|
114
114
|
for (const { key, value } of db.getRange()) {
|
|
115
115
|
if (value && value.pid === pid) {
|
|
116
|
-
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
db.put(String(key), Object.assign(Object.assign(Object.assign({}, value), { lastHeartbeat: now }), (indexState ? { indexState, indexStateAt: now } : {})));
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
}
|
package/package.json
CHANGED