fix(watcher): exclude ignored dirs before watching to prevent inotify exhaustion (#276)
The file watcher registered a recursive watch over the entire project (node_modules, build output, caches included) and filtered only in the callback — exhausting the Linux inotify budget on large repos (#276). It now uses chokidar and excludes the same directories the indexer ignores (built-in default-ignore set + the project .gitignore) BEFORE registering a watch, so the watch count on a 900-dir node_modules drops from ~1200 to ~14 even with no .gitignore. Stacks with the shared daemon (#411): one watcher across agents, now small. Also hardens the #411 daemon lockfile against a concurrent-startup race the new watcher timing made reproducible — the lock is now created atomically with its content (temp-write + hard-link), so racing daemons can never both win. Validated on macOS, Linux (Docker), and Windows (chokidar + fs.linkSync on NTFS). Co-Authored-By: Colby McHenry <me@colbymchenry.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Colby McHenry
Claude Opus 4.7
parent
995da54430
commit
b09b23cf54
@@ -166,6 +166,36 @@ describe('FileWatcher', () => {
|
||||
|
||||
watcher.stop();
|
||||
});
|
||||
|
||||
it('should not watch node_modules even without a .gitignore (#276/#417)', async () => {
|
||||
// No .gitignore in testDir — exclusion relies on the built-in
|
||||
// default-ignore set the indexer uses (buildDefaultIgnore), which a
|
||||
// .gitignore-only filter would miss.
|
||||
fs.mkdirSync(path.join(testDir, 'node_modules', 'dep', 'lib'), { recursive: true });
|
||||
fs.writeFileSync(path.join(testDir, 'node_modules', 'dep', 'index.ts'), 'export const dep = 1;');
|
||||
|
||||
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
|
||||
const watcher = new FileWatcher(testDir, syncFn, { debounceMs: 200 });
|
||||
watcher.start();
|
||||
|
||||
// Let the watcher settle past any residual crawl events.
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
syncFn.mockClear();
|
||||
|
||||
// A source-extension edit INSIDE node_modules must NOT trigger a sync —
|
||||
// the directory was never watched.
|
||||
fs.writeFileSync(path.join(testDir, 'node_modules', 'dep', 'lib', 'extra.ts'), 'export const e = 2;');
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
expect(syncFn).not.toHaveBeenCalled();
|
||||
|
||||
// Positive control: a real source edit still triggers sync, proving the
|
||||
// watcher is live (not merely inert).
|
||||
fs.writeFileSync(path.join(testDir, 'src', 'live.ts'), 'export const live = 3;');
|
||||
await waitFor(() => syncFn.mock.calls.length > 0, 5000);
|
||||
expect(syncFn).toHaveBeenCalled();
|
||||
|
||||
watcher.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
|
||||
Reference in New Issue
Block a user