fix(indexing): replace chokidar file watcher with @parcel/watcher - #12784
Conversation
FileWatcher.initialize() created a chokidar watcher over the whole workspace and awaited its "ready" event with no timeout. orchestrator.ts awaits _startWatcher() before vector-store init and the scan, so the entire indexing pipeline is gated on chokidar reaching "ready". On very large or multi-repo workspaces, when chokidar's native fsevents backend is unavailable (the bundled runtime falls back to a slow per-directory walk), or on symlink-heavy trees (chokidar defaults followSymlinks: true), "ready" may never fire. Because initialize() hangs rather than rejecting, the orchestrator's error handler never runs and indexing sits at "Initializing file watcher..." indefinitely with no error and zero embeddings. - Bound the readiness wait with WATCHER_READY_TIMEOUT_MS (60s); on timeout, reject with an actionable error so the orchestrator surfaces an Error state instead of hanging. Reset watcher state so a later initialize() retries cleanly. - Set followSymlinks: false to match the scanner glob and git semantics, avoiding symlink cycles/amplification that can stall the initial scan. - Extract waitForWatcherReady() and add deterministic unit tests.
| // this rejected promise. | ||
| if (this.ready === ready) this.ready = undefined | ||
| if (this.watcher === watcher) this.watcher = undefined | ||
| void watcher.close() |
There was a problem hiding this comment.
WARNING: Fire-and-forget close() can leak the failed watcher and produce an unhandled rejection
Two concerns on this line, both specific to the failure path this PR adds:
void watcher.close()discards the promise, so ifclose()rejects (quite plausible for a watcher that is already unhealthy — this is exactly the case where the readiness timeout fires) Node reports an unhandled rejection. Attaching a logging.catch()keeps the failure visible without changing control flow.- Because
this.watcheris cleared just above, this is the last reference the class holds to the chokidar instance. Ifclose()never settles, the underlyingfs.watchhandles from the partially-completed initial walk stay open forever, andshutdown()/dispose()can no longer reach them. A retry (which the reset intentionally enables) then creates a second full watcher on top of the first, so repeated retries accumulate watchers and file descriptors.
Something like watcher.close().catch((err) => log.error("failed to close watcher after readiness failure", { err })) addresses (1) and at least makes (2) diagnosable. Also worth noting the add/change/unlink handlers registered above remain attached until close() actually completes, so events from the abandoned watcher can still push entries into accumulatedEvents after initialize() has thrown.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| // own semantics. Following symlinked directories can cause chokidar to | ||
| // re-walk/cycle through link targets, which on symlink-heavy trees can | ||
| // stall the initial scan so "ready" never fires. | ||
| followSymlinks: false, |
There was a problem hiding this comment.
SUGGESTION: followSymlinks: false also stops incremental updates for symlinked files
The comment justifies this by matching the scanner, and for symlinked directories that is accurate (glob defaults to follow: false, so ** does not descend into them). But glob with nodir: true still returns symlinks that point at files, so those paths are indexed by the initial scan. With followSymlinks: false, chokidar watches the link itself rather than the target, so edits to the target no longer emit change — those entries will silently go stale until a full re-index.
Probably an acceptable trade for the hang this fixes, but it's a behavior change beyond "avoid symlink cycles" and worth calling out in the changeset/PR notes (or narrowing, e.g. keep following links but bound the walk).
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| // unavailable (e.g. inside a bundled runtime) and it falls back to a slow | ||
| // per-directory walk, "ready" may never fire. Without a bound this hangs the | ||
| // entire indexing pipeline indefinitely with no surfaced error. | ||
| export const WATCHER_READY_TIMEOUT_MS = 60_000 |
There was a problem hiding this comment.
SUGGESTION: A hard 60s deadline turns a hang into "no indexing at all"
Since orchestrator.runIndexing() awaits _startWatcher() before the vector store init and the scan, a rejection here aborts the whole pipeline and lands in Error state — the user gets zero indexed files instead of an infinite spinner. That is strictly better for diagnosability, but as the PR notes, the watcher is only needed for incremental updates, so a genuinely large monorepo whose initial chokidar walk takes 70s now goes from "eventually works" to "never works".
Given the fix is already touching this path, consider degrading instead of failing: log/telemetry the readiness timeout, skip the watcher (no incremental updates), and let the scan proceed. If the hard failure is intentional for now, a follow-up issue for the degraded mode would be worth linking, and making the bound configurable (as suggested in the PR description) reduces the risk of false failures on slow network filesystems.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| handlers.get(event)?.(arg) | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
SUGGESTION: The riskiest new logic — initialize()'s failure reset — is untested
The three new tests cover waitForWatcherReady in isolation, which is the simple half. The subtler behavior added by this PR is in initialize(): on failure it must clear this.ready and this.watcher, close the watcher, and allow a later initialize() to retry rather than re-awaiting a rejected promise. None of that is exercised, and a regression there reintroduces a stuck state (a retained rejected this.ready makes every subsequent initialize() throw immediately).
A test against a real temp directory with a very small timeout — or injecting the readiness wait — would cover the retry path without a real hang.
Minor note on the fake: once stores handlers in a Map keyed by event name and never removes them, so it does not actually behave like once, and the name createReadyWatcher is misleading given two of the three tests use it for the non-ready cases.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Overview
Incremental review of Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (5 files)
Fix these issues in Kilo Cloud Previous Review Summaries (6 snapshots, latest commit 1d267c6)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 1d267c6)Status: 1 Issue Found | Recommendation: Address before merge Overview
Incremental review of Issue Details (click to expand)SUGGESTION
Files Reviewed (3 files, incremental)
Fix these issues in Kilo Cloud Previous review (commit f15f88b)Status: No Issues Found | Recommendation: Merge Incremental review of Files Reviewed (2 files, incremental)
Previous review (commit 3b4cc06)Status: 2 Issues Found | Recommendation: Address before merge Overview
Incremental review of Issue Details (click to expand)SUGGESTION
Files Reviewed (4 files, incremental)
Fix these issues in Kilo Cloud Previous review (commit 0a12bda)Status: 5 Issues Found | Recommendation: Address before merge Overview
Incremental review of Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (3 files, incremental)
Context read (unchanged): Fix these issues in Kilo Cloud Previous review (commit 07b082e)Status: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (5 files)
Context read (unchanged): Fix these issues in Kilo Cloud Previous review (commit 61ec24d)Status: 5 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (3 files)
Context read (unchanged): Reviewed by grok-4.6 · Input: 152.3K · Output: 36.3K · Cached: 1.4M Review guidance: REVIEW.md from base branch |
The previous chokidar-based FileWatcher blocked the event loop during its initial recursive scan under the bundled bun runtime, so the watcher never emitted "ready" and indexing hung indefinitely at "Initializing file watcher..." with zero files indexed. A setTimeout-based readiness bound (the first attempt on this branch) could not fire because the loop was starved. Verified locally: chokidar v4 under bun starves the event loop on even a ~2.4k-file repo (node handles the same repo in 0.2s), while @parcel/watcher subscribes without a blocking JS-side walk and stays responsive even on a ~15-repo umbrella. A real create/update/delete then flows end-to-end through the incremental pipeline. Switch the indexing FileWatcher to @parcel/watcher — the same native backend the @kilocode/core workspace watcher already uses: - initialize() subscribes (create/update/delete) with no blocking initial scan and no readiness timeout gating indexing startup; it resolves promptly. - The native binding is loaded via createWrapper(require(platform-binding)) for the bundled bun-compiled runtime, with a main-package fallback for dev/tests. - The subscribe function is injectable so the subscribe / event-mapping / retry / teardown paths are unit-tested without a real filesystem watcher. - shutdown()/dispose() unsubscribe with a logging .catch() so a failed teardown cannot surface an unhandled rejection. - Drop the chokidar dependency; add @parcel/watcher; replace WATCHER_READY_TIMEOUT_MS with a defensive PARCEL_SUBSCRIBE_TIMEOUT_MS.
|
Thanks for the review. Validating the original (chokidar + readiness-timeout) approach locally surfaced a deeper problem that changes the fix, so I've pushed a new commit that replaces chokidar with Why the approach changed: under the bundled bun runtime, Against the specific comments:
Local checks: |
| if (!parcel) { | ||
| return Promise.reject( | ||
| new Error( | ||
| "Native file watcher backend (@parcel/watcher) is unavailable; incremental index updates are disabled.", |
There was a problem hiding this comment.
WARNING: A missing/failing native backend now breaks all indexing, not just incremental updates
The message says "incremental index updates are disabled", but this is a rejection, and initialize() propagates it. orchestrator.ts:188 awaits _startWatcher() before vectorStore.initialize() and the scan, so the catch at orchestrator.ts:254 sets Error (Failed during initial scan: ...) and calls stopWatcher() — zero files get indexed.
That's a behavior regression versus chokidar, which was pure JS and always loaded:
- No
@parcel/watcher-<platform>-<arch>prebuild (e.g. freebsd, or an arch/libc combo that isn't published) →loadParcelWatcher()returns undefined → indexing is completely unavailable where it previously worked. - On Linux,
parcel.subscribealso rejects wheninotify_add_watchhitsmax_user_watches(ENOSPC) — a very common failure on large monorepos, and exactly the "large repo" case this PR is fixing.
Since the watcher is only needed for incremental updates, consider degrading instead of failing: log a warning, leave this.subscription unset, and let initialize() resolve so the vector-store init + full scan still run. That preserves the user-visible improvement (a searchable index) even when the native watcher can't be established, and matches what the error message already promises.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| } | ||
| onEvents(events) | ||
| }, | ||
| { ignore: watcherIgnoreGlobs(), backend: watcherBackend() }, |
There was a problem hiding this comment.
SUGGESTION: The ignore list dropped .gitignore / .kilocodeignore, which were a prune, not just a filter
The old chokidar ignored callback consulted this.ignoreInstance too, and chokidar calls ignored for directories, so gitignored trees were never descended into. ignoreInstance is always supplied here (service-factory.ts:260), so this is a real loss.
With only FileIgnore.PATTERNS, @parcel/watcher's inotify backend will now recursively watch every gitignored directory that isn't in that hardcoded list — .venv, venv, .tox, Pods, Carthage, packages/*/build variants, generated code, large data dirs, etc. On Linux each directory costs an inotify watch descriptor, so this can push a big repo past max_user_watches and make subscribe reject with ENOSPC — which (see the comment on line 151) currently fails the whole pipeline.
So it isn't purely an efficiency prune as the doc comment on line 105 claims. Consider deriving extra globs from this.ignoreInstance (or threading an ignore list into FileWatchSubscribe so parcelSubscribe can receive them) to restore the previous pruning behavior.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| } catch (error) { | ||
| // If the timeout won the race, ensure a late-arriving subscription is still | ||
| // torn down instead of leaking. | ||
| void pending.then((subscription) => subscription.unsubscribe()).catch(() => {}) |
There was a problem hiding this comment.
SUGGESTION: Don't swallow the late-teardown failure silently
.catch(() => {}) is an empty rejection handler, which AGENTS.md calls out ("No empty catch blocks") for the same reason it matters here: if a late-arriving subscription fails to unsubscribe(), the watcher genuinely leaks and there is no trace of it anywhere. Every other teardown path in this file already logs (shutdown(), dispose()), so this is the odd one out.
| void pending.then((subscription) => subscription.unsubscribe()).catch(() => {}) | |
| void pending | |
| .then((subscription) => subscription.unsubscribe()) | |
| .catch((err) => log.error("failed to unsubscribe late file watcher subscription", { err, workspacePath: directory })) |
Note this handler also runs when pending itself rejected (not just on timeout), so the log message should read as "best-effort cleanup" rather than implying a leak every time.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Address PR review feedback and trim comments:
- initialize() now warns and continues (the full scan still runs) instead of
rejecting when @parcel/watcher can't be loaded or subscribe fails (e.g. an
inotify ENOSPC on a large Linux repo). The watcher only powers *incremental*
updates, so its absence must not fail the whole index.
- Replace the empty late-teardown `.catch(() => {})` with a best-effort handler
that logs only a genuine late-unsubscribe failure (AGENTS.md: no empty catch).
- Correct the ignore-globs comment: per-repo .gitignore/.kilocodeignore are
enforced in shouldIndex(), not pruned from the native watch.
- Update the failure test to assert graceful degradation instead of throw/retry.
- Trim the comments added by this PR to the minimum.
|
Thanks — addressed the three points in
Verified locally (bun 1.3.14): |
| }) | ||
| this.ready = this.subscribeFn(this.workspacePath, this.onWatchEvents) | ||
| .then((subscription) => { | ||
| this.subscription = subscription |
There was a problem hiding this comment.
WARNING: A subscription that resolves after shutdown()/dispose() is leaked and keeps feeding accumulatedEvents
Neither shutdown() nor dispose() awaits this.ready, and this.subscription is still undefined while a subscribe is in flight. If indexing is cancelled (or stopWatcher() runs) while subscribeFn is still pending — possible for up to PARCEL_SUBSCRIBE_TIMEOUT_MS on a pathological backend — the late resolution lands here and stores a live native subscription on an already-disposed watcher. Nothing ever unsubscribes it, and its events keep hitting handleFileEvent, which appends to accumulatedEvents (line 322) even when not collecting — unbounded growth in a long-lived extension host. Worse, dispose() resets this.ready, so a later initialize() starts a second subscribe; when both resolve, this assignment overwrites the first and leaks it permanently.
Consider capturing the pending promise (const ready = ...) and only storing the subscription when it is still current (if (this.ready === ready) ... else void subscription.unsubscribe()), or awaiting this.ready in shutdown() before tearing down.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| this.subscription = subscription | ||
| log.info("file watcher subscribed", { workspacePath: this.workspacePath }) | ||
| }) | ||
| .catch((error) => { |
There was a problem hiding this comment.
SUGGESTION: Latching the failure into a resolved this.ready removes retry for transient subscribe errors
Because this .catch converts a failed subscribe into a fulfilled ready, every later initialize() (each startIndexing → _startWatcher) no-ops on the early return. A transient failure — e.g. inotify ENOSPC that is freed a minute later — leaves incremental updates disabled until something disposes the watcher (settings change / extension reload). The previous revision reset this.ready = undefined on failure so the next run retried. Consider resetting this.ready in this catch as well (still resolving rather than throwing) so the next indexing run re-attempts the subscription, or call out the trade-off explicitly.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| void pending | ||
| .then((subscription) => (timedOut ? subscription.unsubscribe() : undefined)) | ||
| .catch((err) => { | ||
| if (timedOut) log.warn("failed to tear down late file watcher subscription", { err, workspacePath: directory }) |
There was a problem hiding this comment.
SUGGESTION: A late rejection of pending is logged as a teardown failure, mislabeling the root cause
This .catch handles two different errors: (a) unsubscribe() failing while tearing down a late-arriving subscription, and (b) pending itself rejecting after the timeout already fired. In case (b) the subscribe failed — there was never anything to tear down — yet it is logged as "failed to tear down late file watcher subscription", while initialize() already logged the timeout as the failure reason. The real error is preserved in err, but the message points at the wrong operation; consider distinguishing the cases (e.g. a rejection handler on .then that logs "file watcher subscribe failed after the timeout").
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
There was a problem hiding this comment.
To use Kilo from GitHub you first need to link your GitHub account to Kilo. Link your Kilo account to continue. After linking, mention me again in this issue or pull request.
…ibe lifecycle Addresses the review on the @parcel/watcher switch: - Prune per-repo gitignored directories from the native watch, not just Kilo's infra dirs. initialize() unions the FileIgnore globs with best-effort directory prunes derived from the ignore matcher and threads them into the subscribe call. Without this, inotify watches every gitignored tree (.venv, Pods, generated/data dirs, ...) and a large repo can exhaust max_user_watches (ENOSPC) -- the very failure this PR otherwise only degrades on. The prune is negation-safe: only non-negated, non-glob directory patterns are emitted and any candidate a re-include (!) could reach is dropped, so the prune set stays a subset of never-indexed paths; correctness stays in shouldIndex(). - Fix a subscription leak: a subscribe that resolves after shutdown()/dispose() or is superseded by a newer initialize() is now unsubscribed instead of stored on a disposed watcher, where it would leak and keep growing accumulatedEvents. - Reset this.ready on a subscribe failure so a transient error (e.g. inotify ENOSPC) is retried on the next indexing run rather than latching incremental updates off until reload. - Distinguish a late subscribe rejection from an unsubscribe teardown failure so the timeout path no longer mislabels the root cause. - Add a changeset for @kilocode/kilo-indexing.
| continue | ||
| } | ||
|
|
||
| const scoped = entry.dir ? `${entry.dir}/${body}` : body |
There was a problem hiding this comment.
SUGGESTION: entry.dir is interpolated into watcher globs without escaping
rules() (the correctness path just above) runs directory names through escape() before building patterns, but scoped here — and the globs derived from it on lines 146/149 — uses entry.dir raw. A .gitignore living in a directory whose name contains glob metacharacters (e.g. a Next.js app/blog/[slug]/ route dir with its own ignore file) yields a prune glob like app/blog/[slug]/**/build, where the matcher reads [slug] as a character class. That fails to prune the intended tree and can instead prune an unrelated path such as app/blog/s/... — silently stopping incremental updates for files under it.
Since escape() already exists in this file, consider applying it here (escape(entry.dir)), or skip candidates whose dir matches GLOB_META (under-pruning is the safe direction, as the comment above notes).
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
|
|
||
| if (anchored || body.includes("/")) { | ||
| // Anchored/pathful: a specific subtree, matched relative to root. | ||
| candidates.push({ segment: scoped, glob: scoped, scoped: true }) |
There was a problem hiding this comment.
SUGGESTION: Confirm @parcel/watcher matches ignore globs relative to the subscribed root — these root-relative forms silently no-op if it matches absolute paths
Every glob this PR previously passed to parcel was **/-prefixed (watcherIgnoreGlobs()), which matches regardless of the base the native backend matches against. The two new emission forms here and on line 149 (data, pkg/**/build) are root-relative, so they only prune if parcel matches ignore entries against paths relative to the subscribed directory. If matching is done against full paths (I couldn't verify from this checkout — no installed package — but the native backends traverse full paths), these never fire, and the per-repo gitignore pruning advertised in the changeset would only work for bare root-level names like **/.venv.
The unit tests assert the globs reach subscribe but can't observe what the native backend actually prunes, and this scoped/anchored branch has no emission test either. Worth a quick check against a real subscription; if matching turns out to be absolute, prefixing scoped globs with the workspace root would make them robust under either semantics (core already passes absolute paths to parcel via protecteds()).
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
The gitignore-derived watcher prune globs interpolated the ignore file's directory (entry.dir) raw, unlike rules() which runs it through escape(). A .gitignore inside a directory whose name contains glob metacharacters (e.g. a Next.js `app/.../[slug]/` route) produced a glob like `app/[slug]/**/x` where `[slug]` is a character class, pruning the wrong tree and silently dropping incremental updates for an unrelated path. Skip emitting a prune glob when entry.dir contains glob metacharacters, mirroring the existing skip for metachar pattern bodies; those directories stay watched and are filtered by shouldIndex(). Negations are still collected so shadow-safety is preserved. Also documents that parcel matches glob ignore entries relative to the subscribed root and resolves plain entries to absolute paths, so the anchored plain forms prune correctly. Adds a test for a .gitignore inside a `[slug]` directory.
|
Addressed both suggestions in
Verified locally: |
|
@mavrukin can you check if those are correct? P1 / merge blocker: native ignore globs can crash the indexing worker on Windows. P1: repository names can alter Parcel glob semantics and silently disable incremental indexing. P2: teardown retains a native process-exit risk. |
|
Could we simplify the Parcel loader to match the existing implementation in
That gives us one predictable loading path and keeps the indexing watcher aligned with the already-shipped core watcher. |
…el loader Addresses review feedback from @marius-kilocode: - Add `!` to the glob-metacharacter guard in pruneGlobs(). A repository whose directory name contains `!` (e.g. `!scope`) previously emitted a prune glob like `!scope/**/generated`, which parcel reads as a negation and could suppress unrelated watcher events, silently leaving the index stale. `!` is now skipped alongside the other metacharacters (matching discovery()'s set), so such directories stay watched and are filtered by shouldIndex(). - Simplify the parcel loader to match @kilocode/core's watcher: hoist createWrapper to a static top-level import from `@parcel/watcher/wrapper`, keep the platform binding as a dynamic require (bun needs it for compiled multi-platform binaries), and drop the `@parcel/watcher` main-package fallback so there is one loading path that degrades cleanly when the native binding is unavailable.
| parcelModule = createWrapper(binding) as typeof import("@parcel/watcher") | ||
| } catch { | ||
| // Single loading path: degrade cleanly when the native binding is unavailable | ||
| // (no @parcel/watcher main-package fallback), exactly like the core watcher. |
There was a problem hiding this comment.
SUGGESTION: PR description still advertises the removed main-package fallback
The Implementation section of the PR description says the native binding loads "with a main-package fallback for dev/CLI/tests", but this commit deletes that fallback — loadParcelWatcher() now has a single loading path and degrades when the platform binding is missing. The simplification itself is fine (it does mirror packages/core/src/filesystem/watcher.ts exactly, and the degrade path is covered by tests), but the stale description will send reviewers looking for a fallback that no longer exists. Worth updating the PR body.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
|
Thanks @marius-kilocode — verified all four against the code and both upstream issues. Two are fixed in ✅ Fixed
Loader simplification. Done, mirroring 🔺 Verified — upstream Parcel items, not this change #250 (glob ignore → native stack overflow on long paths). Yep, confirmed. But it's already latent in the shipped #258 (crash at process exit, static destruction order). Confirmed too, and the same story: an upstream Parcel exit-time defect that affects any embedder including the core watcher, not introduced by this change. Net: |
marius-kilocode
left a comment
There was a problem hiding this comment.
Thanks for sticking with this and working through the feedback, @mavrukin. Verified the compiled build in isolated VS Code on macOS: 2,502-file startup, search, atomic saves, folder moves/replacements/deletes, and ignore re-includes all passed with local test providers.
|
I also applied another fix to get this through. Some of the pruning will be broken on Windows due to the |
Thanks appreciate it |
Issue
Fixes #12783
Context
Local codebase indexing hangs indefinitely at "Initializing file watcher…" —
zero files indexed, no error surfaced — on large or multi-repo workspaces.
Root cause (validated locally): the indexing
FileWatcherused chokidar,and under the bundled bun runtime
chokidar.watch()blocks the event loopduring its initial recursive scan. On even a 2,438-file repo the loop is starved
so badly that nothing runs — no
ready, and nosetTimeoutcan fire (so thisbranch's original readiness-timeout couldn't help). Node handles the same repo in
~0.2s; bun starves indefinitely. Because the orchestrator awaits the watcher
before vector-store init and the scan, the whole indexing pipeline is gated on a
watcher that never becomes ready.
Implementation
Replace chokidar with
@parcel/watcher— the same native, event-based backend@kilocode/core's workspace watcher already uses (and which runs fine under bun) —and harden the subscribe lifecycle around it.
initialize()subscribes forcreate/update/deletewith no blocking initial scan and resolves promptly. The loader mirrors
@kilocode/core's watcher: a staticcreateWrapperimport from@parcel/watcher/wrapperplus a dynamicrequireof the platform binding(
@parcel/watcher-<platform>-<arch>, bun-compile-safe) — a single load path thatdegrades cleanly to "no watcher" when the native binding is unavailable (no
main-package fallback).
#151). If the backend is missing orsubscriberejects(e.g. inotify
ENOSPC),initialize()warns and resolves so the full scan stillproduces a searchable index; a transient failure clears
this.readyso the nextrun retries.
#245). A subscribe that resolves aftershutdown()/dispose(), or is superseded by a newerinitialize(), is torn downvia an identity guard instead of stored on a disposed watcher.
#134). Kilo's infra dirs plusper-repo
.gitignore/.kilocodeignoredirectory patterns are passed toparcel's
ignore, so large repos don't over-watch (the very thing that triggersthe
ENOSPCdegrade). This is a watch-descriptor optimization only — correctnessstays in
shouldIndex()— so it is deliberately conservative: negated (!)gitignore patterns are honored, and any ignore-file directory whose name contains
glob metacharacters (a leading
!negation, or e.g. a Next.js[slug]route) isskipped, so a derived prune can never invert into a negation or match the wrong tree.
chokidar; add@parcel/watcher@2.5.1; replaceWATCHER_READY_TIMEOUT_MSwith a defensive
PARCEL_SUBSCRIBE_TIMEOUT_MS. The subscribe fn is injectable, sothe whole lifecycle is unit-tested without a real watcher.
Tradeoff worth reviewer attention: parcel's
ignorecan't express gitignorenegation/ordering, so the derived prune globs intentionally under-prune (skip
anything a re-include could reach) rather than risk silently dropping an indexed file.
Screenshots / Video
N/A — no UI change. The indexing status text is unchanged; the fix is that it now
progresses to completion instead of hanging.
How to Test
Manual/local verification
FileWatcherand ranit against a real ~15-repo umbrella (~275k indexable files).
initialize()resolvesin ~0.2s (was: indefinite hang), the native subscribe succeeds, 879
gitignore-derived directory prunes are applied, and a real create event flows
end-to-end through the pipeline.
tsgotypecheck clean;bun test … file-watcher.test.ts→ 17/17 pass; full
bun turbo typecheck(JetBrains incl.) green;oxlint→ 0errors;
prettier→ clean.confirmed in the live VS Code extension that indexing runs to completion — status
reaches "IDX Complete — File watcher started. Index up-to-date." — where it
previously hung indefinitely at "Initializing file watcher…".
Reviewer test steps
packages/kilo-indexing/:bun run typecheck, thenbun test test/kilocode/indexing/processors/file-watcher.test.ts(17 pass).update→change), ignore/extension filtering, gitignore-prune forwarding (incl. negationand
[slug]/!scope-metachar-dir skips), degrade-on-failure + retry, andlate-resolution teardown.
Blocked checks and substitute verification
@parcel/watchernativesubscription isn't exercised by them. Substitute verification: confirmed the real
subscription end-to-end on the ~15-repo umbrella and in the packaged extension
(above), and confirmed parcel's
ignorematching semantics from itswrapper.js(globs matched relative to the subscribed root; plain entries resolved to absolute).
Checklist
.changeset/kilo-indexing-file-watcher.md)