fix: default watcher to polling on Windows to avoid ReFS BSOD#778
fix: default watcher to polling on Windows to avoid ReFS BSOD#778carlos-alm merged 3 commits intomainfrom
Conversation
fs.watch with recursive:true calls NtNotifyChangeDirectoryFileEx, which can crash the ReFS driver on Windows Dev Drives causing HYPERVISOR_ERROR (20001) blue screens. Default to stat-based polling on Windows (process.platform === 'win32'). Native fs.watch remains the default on macOS/Linux. Users can override with --poll (force polling) or --native (force OS watchers).
Greptile SummaryThis PR defaults Confidence Score: 5/5Safe to merge; the core fix is correct and previous P1 concerns are resolved — remaining findings are P2 style issues. Both open findings are P2: one is an edge case for invalid CLI input (NaN poll interval), the other is a minor hidden-directory filtering inconsistency between modes. Neither affects correctness or data integrity for typical usage. Prior P1 concerns (Commander default injection, silent flag conflict) are fully addressed. src/domain/graph/watcher.ts — collectTrackedFiles hidden-directory filtering differs from native mode Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[codegraph watch] --> B{--poll AND --native?}
B -- yes --> ERR[program.error: mutually exclusive]
B -- no --> C{resolve poll mode}
C -- "--poll" --> POLL[poll = true]
C -- "--native" --> NATIVE[poll = false]
C -- neither --> AUTO{process.platform === 'win32'?}
AUTO -- yes --> POLL
AUTO -- no --> NATIVE
POLL --> D[Polling mode\nsetInterval every POLL_INTERVAL_MS]
D --> E[collectTrackedFiles: sync recursive readdirSync]
E --> F[Diff mtimes vs mtimeMap]
F --> G[Add changed/new/deleted files to pending]
G --> H{pending.size > 0?}
H -- yes --> I[debounce 300ms → processPendingFiles]
H -- no --> D
NATIVE --> J[fs.watch recursive\nnative OS watcher]
J --> K[shouldIgnore + isTrackedExt filter]
K --> L[Add to pending]
L --> M[debounce 300ms → processPendingFiles]
I --> N[rebuildFile → journal → changeEvents]
M --> N
N --> O[SIGINT → cleanup + closeDb + exit]
Reviews (2): Last reviewed commit: "fix: add --poll/--native mutual exclusio..." | Re-trigger Greptile |
src/cli/commands/watch.ts
Outdated
| options: [ | ||
| ['--poll', 'Use stat-based polling (default on Windows to avoid ReFS/Dev Drive crashes)'], | ||
| ['--native', 'Force native OS file watchers instead of polling'], | ||
| ['--poll-interval <ms>', 'Polling interval in milliseconds (default: 2000)', '2000'], |
There was a problem hiding this comment.
--poll-interval default is always truthy, undefined is never passed
Because Commander.js injects the third argument ('2000') as the option's default, opts.pollInterval is always the string '2000' — it is never undefined or empty. The opts.pollInterval ? ... : undefined guard on line 21 is therefore always true, and pollInterval: 2000 is always forwarded to watchProject. This is harmless today (both defaults match), but it means the watcher's internal default can never take precedence via the CLI if it changes later. The cleanest fix is to remove the default from the Commander option definition.
There was a problem hiding this comment.
Fixed — removed the Commander.js default value from the --poll-interval option definition so opts.pollInterval is undefined when not explicitly passed by the user, allowing the watcher's internal default (2000ms) to take precedence.
| const engine = ctx.program.opts().engine; | ||
| await watchProject(root, { engine }); | ||
| // Explicit --poll or --native wins; otherwise let watcher auto-detect by platform | ||
| const poll = opts.poll ? true : opts.native ? false : undefined; |
There was a problem hiding this comment.
Conflicting
--poll and --native flags are silently resolved
When a user passes both --poll and --native, --poll wins without any warning. A simple guard or Commander's .conflicts() API would make this explicit and prevent user confusion:
if (opts.poll && opts.native) {
ctx.program.error('--poll and --native are mutually exclusive');
}There was a problem hiding this comment.
Fixed — added a mutual exclusion guard that calls ctx.program.error() when both --poll and --native are passed, giving users a clear error message instead of silently picking one.
Summary
fs.watchwithrecursive: truecallsNtNotifyChangeDirectoryFileExon Windows, which can crash the ReFS driver on Dev Drives causing HYPERVISOR_ERROR (20001) blue screensprocess.platform === 'win32'), nativefs.watchremains default on macOS/Linux--pollflag to force polling on any platform,--nativeto force OS watchers, and--poll-interval <ms>to tune polling frequencyTest plan
codegraph watchon Windows uses polling by defaultcodegraph watch --nativeon Windows uses fs.watchcodegraph watch --pollon macOS/Linux uses polling