fix(watcher): bound fd/watch cost with a native fs.watch hybrid (#644, #496, #555, #628)#650
Merged
Merged
Conversation
…#496, #555, #628, #579) chokidar v4 holds one OS file descriptor per watched file on macOS (libuv's kqueue backend registers an fd per vnode; fsevents is installed but v4 no longer uses it). On a large project the `serve --mcp` daemon accumulated tens of thousands of open REG descriptors and exhausted kern.maxfiles — crashing unrelated processes system-wide with ENFILE. #276 only trimmed the count by ignoring directories; the source tree still cost one fd per file. Replace chokidar with a pure-JS native fs.watch hybrid, keeping codegraph's zero-native-addon "any OS builds any bundle" invariant: - macOS / Windows: a single recursive fs.watch (one FSEvents stream / ReadDirectoryChangesW handle) -> O(1) descriptors regardless of repo size. - Linux: one inotify watch per directory (O(dirs), dynamic add for new dirs, capped via CODEGRAPH_MAX_DIR_WATCHES) instead of per-file watches. Validated empirically: macOS 0 extra fds at 6k and 12k files; Linux 31 inotify watches at 6k files (per-file would be 6k); Windows recursive catches nested and new-directory edits. Full test suite green. Tests drive the watcher through an inertForTests seam (no OS watcher) for determinism under parallel vitest, with one real-fs end-to-end test exercising the genuine native path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the macOS file-descriptor exhaustion that took down users' whole machines (#644, #496, #555), plus the related resource-guardrail ask (#628) and the Linux inotify-count half of #579.
Root cause — found by running +
lsof, not reading:chokidar v4holds one OS file descriptor per watched file on macOS. libuv's kqueue backend registers an fd per vnode, andfseventsis installed but v4 no longer uses it — so the source looks innocent. 6,000 files = 6,000 fds, dead linear. That exhaustskern.maxfiles(default ~10–25k) and produces the system-wideENFILEthat made unrelated apps (shell, editor, Docker, browser) fail with "too many open files" until the codegraph daemon was killed. #276 only reduced the count by ignoring directories; the source tree itself still cost 1 fd/file.Fix
Drop chokidar; rewrite the watcher as a pure-JS native
fs.watchhybrid — no new dependency, and it keeps the project's zero-native-addon "any OS builds any bundle" invariant intact:fs.watch(FSEvents / ReadDirectoryChangesW)CODEGRAPH_MAX_DIR_WATCHEScap)Validation (empirical, all three platforms)
FileWatcher+lsofFileWatcherin Docker,/proc/<pid>/fdinfoFull test suite green (57 files, 1,112 tests). Watcher tests use an
inertForTestsseam for determinism under parallel vitest, with one real-fs end-to-end test exercising the genuine native path.Scope
Fixes #644
Fixes #496
Fixes #555
Fixes #628
🤖 Generated with Claude Code