/** * Watch-root planner — reduce the in-scope tree to a small set of OS watches. * The default plan is exact. A caller may set `maxDepth` to trade additional * filtered events for fewer recursive watch registrations. * * Why this exists: on macOS/Windows the watcher used to place ONE recursive * `fs.watch` on hqRoot. That is cheap in handles but the OS then delivers every * event in the tree, including the buckets sync never uploads — on a real HQ * root that is `repos/` (~26k directories) and `workspace/worktrees/` (~62k), * where agent builds, installs, and checkouts churn constantly. Every one of * those events woke the runner, cost a `path.resolve` (plus a synchronous * `lstat` for renames), and was then dropped by the emit filter. The result was * a node process pinned above 100% CPU doing nothing but allocating and * garbage-collecting discarded paths. * * The exact planner walks the exclusion filter ONCE at start and returns: * - `recursive`: directories whose entire subtree is in scope. One recursive * watch each; with no depth bound, the OS never reports an excluded path * under them. * - `shallow`: directories that contain an excluded descendant. Watched * non-recursively so their own files still fire, with their in-scope * children planned separately. * * The walk is post-order: whether a directory can collapse to a single * recursive watch depends on its whole subtree, not just its direct children * (`workspace/` looks clean until you reach `workspace/worktrees/`). */ import type { WatchPathFilter } from "./watcher.js"; export interface WatchRootPlan { /** Directories to watch recursively — their whole subtree is in scope. */ recursive: string[]; /** Directories to watch non-recursively — they contain excluded children. */ shallow: string[]; } export interface PlanWatchRootsOptions { /** * Optional depth at which the planner stops splitting and takes a recursive * watch even if the subtree still holds exclusions. Unbounded by default. * The planner still walks every included directory at the bound so it can * seed known kinds and find symlinks that must remain outside recursive * roots. A bound trades fewer OS registrations for additional events; the * caller's event-time filter drops excluded residue, so this is a performance * dial, never a correctness one. */ maxDepth?: number; /** * Whether the platform can watch a directory recursively with one handle. * Defaults to true (macOS FSEvents, Windows ReadDirectoryChangesW). Linux * has no such handle, so with `false` the walk never collapses a subtree: * every in-scope directory becomes its own shallow watch and `recursive` is * always empty. The plan's size is then the in-scope DIRECTORY count — the * unit that actually costs an inotify watch — never the file count. * `maxDepth` is ignored in this mode: there is no recursive watch to take at * the bound, so stopping early would leave the deeper tree unwatched. */ recursiveWatches?: boolean; /** Directory reader seam — defaults to a real `readdirSync`. */ listChildDirs?: (dir: string, onSymlinkEntry?: (absolutePath: string) => void) => string[]; /** * Invoked once for every in-scope directory the walk visits. Lets a caller * populate its own directory index in the SAME pass instead of walking the * tree a second time. */ onDirectory?: (absolutePath: string) => void; } /** * Plan the watch roots for `hqRoot` under `shouldEmit`. * * `shouldEmit(dir, true)` is the same directory predicate the watcher uses at * event time, so the plan and the emit filter can never disagree about what is * in scope. */ export declare function planWatchRoots(hqRoot: string, shouldEmit: WatchPathFilter, opts?: PlanWatchRootsOptions): WatchRootPlan; /** * True when `absolutePath` already falls under one of `recursiveRoots`. * * Walks the path's own ancestors rather than scanning the root set: this runs * on every rename event, in the hot path of a backend whose entire purpose is * cutting per-event cost, and a real HQ tree plans ~685 roots. Ancestor-walking * is O(path depth) — about ten set lookups — instead of O(roots) string * comparisons. It also cannot fall into the shared-prefix trap, where `/hq/a/bc` * looks like it sits under the root `/hq/a/b`. */ export declare function isCoveredByRecursiveRoot(absolutePath: string, recursiveRoots: ReadonlySet): boolean; //# sourceMappingURL=watch-roots.d.ts.map