grepmax 0.26.72 → 0.26.74
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/dist/commands/watch.js +15 -4
- package/dist/lib/daemon/daemon.js +28 -14
- 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/utils/host-guard-policy.js +1 -0
- package/dist/lib/utils/kernel-zone.js +4 -4
- package/dist/lib/utils/pressure-diagnostics.js +176 -0
- package/dist/lib/utils/resource-budget.js +12 -9
- package/dist/lib/utils/safety-latch.js +5 -1
- package/dist/lib/utils/watcher-store.js +3 -2
- package/dist/lib/workers/pool.js +13 -6
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -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;
|