/** * Watch-root planner — reduce the in-scope tree to the smallest set of OS * watches that covers exactly the paths that can upload. * * 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 planner walks the exclusion filter ONCE at start and returns: * - `recursive`: directories whose entire subtree is in scope. One recursive * watch each; 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 walk stops splitting and takes a recursive * watch even if the subtree still holds exclusions. Unbounded by default: * the walk costs about what {@link TreeWatcher}'s known-kinds seed walk * already cost on every start, and the two are fused via `onDirectory`, so * paying for full pruning is free. A bound trades pruning for walk time on * pathologically deep trees; the per-event filter drops whatever residue it * admits, 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) => 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