# How the Agentsview File Watcher Handles Recursive Watching with a Budget

> Learn how the agentsview file watcher efficiently handles recursive watching with a budget. Discover its resource-aware design and metrics for optimal performance. Get started today.

- Repository: [Kenn Software/agentsview](https://github.com/kenn-io/agentsview)
- Tags: internals
- Published: 2026-06-20

---

**The agentsview file watcher implements a budget-aware recursive directory monitor using fsnotify that gracefully stops traversing subdirectories when it hits a caller-defined limit or system resource constraints, returning detailed metrics about watched, unwatched, and exhausted paths.**

The `kenn-io/agentsview` repository provides a robust synchronization layer that must monitor directory trees without overwhelming the operating system. By leveraging a **budget mechanism**, the watcher prevents the `EMFILE` (too many open files) and `ENOSPC` (no space left on device) errors that typically plague recursive filesystem monitors on Linux and macOS.

## Core Implementation in [`internal/sync/watcher.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sync/watcher.go)

The recursive watching logic centers on [`internal/sync/watcher.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sync/watcher.go), which orchestrates tree traversal while respecting configurable limits.

### `WatchRecursiveBudgeted`: The Budget-Aware Walker

The **`WatchRecursiveBudgeted`** method walks a directory tree using `filepath.WalkDir` and adds each subdirectory to an `fsnotify.Watcher` until one of three stop conditions occurs (lines 103–113):

1. **Budget exhaustion** – When the caller-supplied maximum directory count (`budget`) reaches zero, the function sets `result.BudgetExhausted = true` and aborts the walk by returning `filepath.SkipAll` (lines 103–105).

2. **Resource exhaustion** – If `fsnotify.Add` returns an error matching `syscall.EMFILE` or `syscall.ENOSPC`, the watcher records the failure in `result.ResourceExhausted` along with the specific path, then stops the traversal (lines 108–113).

3. **Normal completion** – Successfully added directories increment `result.Watched`, while non-resource exhaustion failures increment `result.Unwatched` (lines 107–110).

### Unbounded Watching via `WatchRecursive`

For callers requiring unlimited recursion, the **`WatchRecursive`** method forwards to `WatchRecursiveBudgeted` with `math.MaxInt` as the budget (lines 76–78). This provides a convenience API while maintaining a single implementation path for both bounded and unbounded scenarios.

## Handling Resource Limits and Exclusions

Beyond the budget counter, the watcher includes safeguards to handle filesystem edge cases and user-defined filters.

### Detecting System Resource Exhaustion

The helper **`isWatchResourceExhaustion`** abstracts OS-specific error checks, reliably identifying when the underlying `fsnotify` library hits kernel-imposed watch limits. This allows `agentsview` to distinguish between permission errors (which might be temporary) and fundamental resource constraints (which require alternative strategies like polling).

### Directory Exclusion Logic

Before adding a directory to the watch list, the traversal invokes **`shouldExclude…`** helpers that check against user-provided ignore patterns (such as `.git` or `node_modules`). This pre-filtering conserves the budget for directories that actually require monitoring.

### Root Path Management

The methods **`addRoot`** and **`addShallowRoot`** maintain an internal registry of root paths. This registry enables correct resolution of exclusion checks and supports shallow-watch semantics, ensuring that the watcher correctly handles overlapping or nested root configurations.

## Practical Usage Example

The following pattern demonstrates how to instantiate the watcher with a 500 ms debounce period and enforce a strict budget of 200 directories:

```go
// Create a watcher with a 500 ms debounce period.
w, _ := sync.NewWatcher(500*time.Millisecond, func(paths []string) {
    // Trigger a sync for the changed paths.
    fmt.Println("Changed:", paths)
}, []string{".git", "node_modules"})

// Watch a directory tree, but only allow up to 200 watches.
result := w.WatchRecursiveBudgeted("/home/user/agents-sessions", 200)

fmt.Printf("watched=%d unwatched=%d budgetExhausted=%t resourceExhausted=%t\n",
    result.Watched, result.Unwatched,
    result.BudgetExhausted, result.ResourceExhausted)

// Start processing events.
w.Start()
defer w.Stop()

```

If `result.BudgetExhausted` returns `true`, the caller can implement a fallback polling mechanism for the remaining subdirectories, ensuring no filesystem events are missed despite the budget constraint.

## Summary

- **`WatchRecursiveBudgeted`** in [`internal/sync/watcher.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sync/watcher.go) provides the core budget-aware traversal using `filepath.WalkDir`.
- The watcher stops traversal immediately upon **budget exhaustion** (`result.BudgetExhausted = true`) or **resource exhaustion** (`result.ResourceExhausted = true`), preventing OS-level `EMFILE`/`ENOSPC` errors.
- **Unbounded watching** is implemented as `WatchRecursive` calling the budgeted variant with `math.MaxInt`.
- Exclusion patterns and root management helpers (`shouldExclude…`, `addRoot`, `addShallowRoot`) optimize the allocation of limited watch resources.
- The design supports graceful degradation to polling when directory trees exceed system or configured limits.

## Frequently Asked Questions

### What happens when the agentsview file watcher hits the budget limit?

When the directory count exceeds the supplied budget, `WatchRecursiveBudgeted` immediately sets `result.BudgetExhausted = true` and returns `filepath.SkipAll` to halt the walk (lines 103–105). The function returns to the caller with statistics on directories watched so far, allowing the application to decide whether to poll the remaining paths or notify the user.

### How does the watcher handle system-level file descriptor limits?

The watcher detects `syscall.EMFILE` and `syscall.ENOSPC` errors through the `isWatchResourceExhaustion` helper. Upon encountering these errors during `fsnotify.Add`, it records the failure in `result.ResourceExhausted`, captures the offending path, and aborts the recursive walk (lines 108–113). This prevents the process from crashing when the kernel's `fs.inotify.max_user_watches` limit is reached.

### Can I exclude specific directories from recursive watching?

Yes. The `NewWatcher` constructor accepts a slice of ignore patterns (such as `[]string{".git", "node_modules"}`). Internal `shouldExclude…` helpers check these patterns during the `filepath.WalkDir` traversal, skipping matched directories before they consume any of the watch budget.

### What is the difference between `WatchRecursive` and `WatchRecursiveBudgeted`?

`WatchRecursive` is an unbounded convenience method that calls `WatchRecursiveBudgeted` with `math.MaxInt` as the budget (lines 76–78), effectively attempting to watch the entire tree. `WatchRecursiveBudgeted` requires an explicit integer budget and returns granular metrics (`Watched`, `Unwatched`, `BudgetExhausted`, `ResourceExhausted`), making it suitable for production environments where resource constraints must be strictly enforced.