| name | parcel |
| description | Kolu's project-wide default for filesystem monitoring is `@parcel/watcher`. Reach for this skill when adding or modifying any code that watches files or directories — recursive subtree watching, single-file observation, fs.watch alternatives, chokidar replacement, inotify/FSEvents/watchman backend selection, ignore globs, watcher debouncing, or refcounted shared subscriptions. Covers backend dispatch, the watchman invocation path, ignore handling, post-install reconciliation, and the failure modes Kolu's logger surfaces. |
@parcel/watcher integration
@parcel/watcher is Kolu's default filesystem watcher. Reach for it instead
of chokidar, raw fs.watch, or hand-rolled polling whenever a feature
needs to observe a directory subtree. Today's only consumer is the
working-tree watcher (packages/integrations/git/src/working-tree-watcher.ts)
— the git-dir watchers (head-watcher, reflog-watcher, index-watcher)
use plain fs.watch via kolu-io's refcounted-dir-watcher.ts because they
target a single known file inside .git/, where parcel-watcher's recursive
model would be overkill. New fs-monitoring code should default to parcel-watcher
unless it has a similarly narrow target.
Backend dispatch
Source: node_modules/.pnpm/@parcel+watcher@2.5.6/node_modules/@parcel/watcher/src/Backend.cc:30-69.
Kolu pins the OS-native backend explicitly (inotify / fs-events /
windows, chosen off process.platform — see PARCEL_BACKEND in
working-tree-watcher.ts). It does not use backend: "default". The pin
skips the auto-dispatch order below — most importantly the WatchmanBackend
probe, whose per-subscribe popen leaks a zombie on watchman-less hosts (see
next section). If you write a new parcel consumer, pin the backend the same way.
backend: "default" would select in this order, first match wins:
FSEvents on macOS — native recursive, one stream per repo.
WatchmanBackend if WatchmanBackend::checkAvailable() returns true.
WindowsBackend on Windows — native recursive ReadDirectoryChangesW.
InotifyBackend on Linux — one inotify slot per non-ignored directory.
KqueueBackend on BSD.
BruteForceBackend — periodic full-tree stat; the fallback fallback.
So under "default" on Linux, watchman is probed (and preferred if reachable)
before inotify; on macOS watchman is never used (FSEvents wins). The explicit
pin lands directly on step 1/3/4 for the platform and never runs step 2's probe.
How parcel invokes watchman
Source: src/watchman/WatchmanBackend.cc.
-
checkAvailable() (line 107) just calls watchmanConnect().
-
getSockPath() (line 43) does the only watchman binary invocation:
popen("watchman --output-encoding=bser get-sockname 2>/dev/null", "r")
then parses BSER output for the sockname field. If WATCHMAN_SOCK env
var is set, that wins and the binary isn't run at all.
⚠ Zombie leak — why Kolu pins the backend (juspay/kolu#1691). On a
host without watchman, this popen forks /bin/sh, the watchman exec
fails, and parcel's error path returns without pclose() — so the
sh is never wait()ed. popen bypasses child_process, so Node's
libuv can't reap it either: it lingers as a zombie forever, one per
subscribe. Free on CPU/RAM but a long-lived daemon (a remote-bound
padi serving a full e2e run) accumulates dozens and the event loop drags.
Pinning the native backend (above) skips checkAvailable() entirely, so
the popen never runs. Verified: per-subscribe leak → 0.
-
From there it's a Unix-domain socket carrying BSER-encoded JSON. No more
subprocess spawns.
Commands (BSER arrays sent over the socket):
| Source | Command | Purpose |
|---|
watchmanWatch (line 100) | ["watch", "/abs/dir"] | start tracking |
clock (line 230) | ["clock", "/abs/dir"] | get baseline clock token |
subscribe (line 281) | ["subscribe", "/abs/dir", "parcel-<ptr>", {fields,since,expression}] | start receiving events |
unsubscribe (line 330) | ["unsubscribe", "/abs/dir", id] | tear down |
Subscription IDs are "parcel-" + hex(watcher-pointer). Event mapping in
handleFiles (line 137):
new && exists → create
exists && !S_ISDIR(mode) → update
!new && !exists → remove
Ignore globs are translated to a watchman expression
["not", ["anyof", ["dirname", rel], ...]] only for ignores that are direct
subpaths of the watched root (line 300). Glob-style ignores like
**/node_modules are filtered client-side after events arrive — pass them in
the ignore option but don't expect watchman-side filtering.
Cookie files (.watchman-cookie-*)
Written by the watchman daemon, not parcel-watcher. The daemon drops these
files inside watched dirs to verify it can observe its own writes
(watch/query commands trigger the dance). Normally ephemeral. Stragglers
mean the daemon was killed mid-handshake. Add .watchman-cookie-* to
.gitignore if watchman is in use.
Kolu's runtime status
As of #788, Kolu does not ship watchman with the production binary. The
nix run wrapper (default.nix:156) only adds nodejs git gh to PATH — so
even under backend: "default" the watchman probe would always fail and parcel
would fall through to inotify on Linux / FSEvents on macOS. Since #1692, Kolu
doesn't rely on that fall-through: it pins the native backend, so
checkAvailable() (and its leaking popen) never runs at all. Issue #788
tracks the watchman integration work; if it lands, the pin is where you'd
re-enable watchman deliberately (with a WATCHMAN_SOCK that avoids the probe).
Kolu's wrapper invariants
packages/integrations/git/src/working-tree-watcher.ts:
- Refcounted shared singleton per
repoRoot — N callers → one parcel
subscription, listener-side filtering by optional filePath.
- Trailing-edge debounce at
WATCHER_DEBOUNCE_MS (150ms) coalesces bursts
into one fire per listener.
- Hard-coded
IGNORE_GLOBS — .git, node_modules, dist, build,
target, .next, .turbo, .cache, .parcel-cache, .kolu-dev,
.kolu-state, .DS_Store. Not gitignore-aware. Over-firing on user build
outputs is absorbed by the snapshot-equality check in
streamSnapshots(...) upstream.
- Post-install reconciliation tick (line 176) —
parcelSubscribe is
async. Filesystem mutations between subscribe() call and parcel resolving
are invisible to parcel; the streaming endpoint already yielded its initial
snapshot. The reconciliation fires every current listener once parcel is
ready, so consumers re-read state and catch the missed window. Without this
the client sees a stale view that no future event corrects.
cancelled guard on the .then — if the last subscriber unsubscribed
before parcel resolved, late-unsubscribe the AsyncSubscription instead of
storing it.
What Kolu logs
Watcher lifecycle through Logger (kolu-shared). Grep these strings to verify
the watcher came up in production:
info git: working-tree watcher installed ← parcel resolved successfully
info git: working-tree watcher retired ← last subscriber gone
error git: working-tree watcher install failed ← parcel subscribe() rejected
error git: working-tree watcher callback error ← parcel reported event-stream error
error git: working-tree late-unsubscribe failed
error git: working-tree unsubscribe failed
error git: working-tree listener threw
Individual filesystem events are not logged — too noisy. The bucket-and-
debounce path swallows event paths silently.
Failure modes worth knowing
- Container/WSL2 bind mounts — inotify/FSEvents may be unavailable.
Nuance since the backend pin (#1692):
BruteForceBackend (~1s polling) is
parcel's compile-time-last-resort backend — under "default" it's reached
only when no earlier backend is compiled in, not when a compiled one
fails to construct at runtime. On Kolu's Linux build INOTIFY is compiled,
so "default" selects InotifyBackend (after the failed watchman probe),
and a bind mount that can't inotify makes both "default" and the pinned
"inotify" fail identically (error git: working-tree watcher install failed) — the pin forgoes no polling path there. On the three targets
the pin is behaviorally identical to "default"'s post-probe outcome, minus
the leak. On an untargeted platform (not linux/darwin/win32) the pin
throws at module init (unsupported platform …) rather than degrade to a
silent poller — kolu targets exactly those three, and the repo's fail-fast
philosophy forbids a graceful-degradation fallback. (If a BSD/other target
ever became real, map it to its native backend — kqueue etc. — not to a
poller.) Net: leak-free everywhere, and no platform silently degrades.
- Linux inotify slot exhaustion — kernel default is
fs.inotify.max_user_watches=8192. A typical Kolu repo uses ~500–2000
slots; multiple worktrees compound. Watchman amortizes this across one
daemon (#788).
detect-libc glibc/musl picker runs on require('@parcel/watcher')
(index.js:5). The Nix build needs the matching native binary in
node_modules — confirm @parcel/watcher-linux-x64-glibc is present
under node_modules/.pnpm/.
dontFixup = true in default.nix skips patchELF on the native .node
binary. Today the @parcel/watcher binary loads its own libstdc++ via
fallback paths and works, but if a future parcel-watcher version pulls in
a harder dynamic-link requirement, expect to revisit this.
Quick references
- Backend dispatch:
node_modules/.pnpm/@parcel+watcher@2.5.6/node_modules/@parcel/watcher/src/Backend.cc:30
- Watchman invocation:
node_modules/.pnpm/@parcel+watcher@2.5.6/node_modules/@parcel/watcher/src/watchman/WatchmanBackend.cc:43
- Kolu wrapper:
packages/integrations/git/src/working-tree-watcher.ts
- Composed watcher API:
packages/integrations/git/src/repo-change.ts
- Watchman integration issue: juspay/kolu#788
- Streaming endpoints that consume the watcher:
packages/server/src/router.ts
(onStatusChange, onDiffChange, onListAllChange, onReadFileChange)