How AgentsView Detects and Syncs New Agent Sessions: The File Watcher Mechanism Explained
AgentsView uses a debounced file watcher built on Go's fsnotify library to monitor session directories, automatically detect new agent sessions, and trigger database synchronization through a callback-driven pipeline.
The kenn-io/agentsview repository maintains a real-time SQLite store by watching directories that contain agent session files. The file watcher mechanism lives in internal/sync/watcher.go and wraps the underlying fsnotify package with recursive directory monitoring, resource exhaustion handling, and intelligent debouncing to batch rapid filesystem changes.
Core Architecture in internal/sync/watcher.go
The watcher implementation centers on a struct that manages an fsnotify.Watcher instance alongside configuration for debounce intervals and exclusion patterns. When initialized via NewWatcher (lines 47-69), the system stores a change callback function (onChange) that bridges filesystem events to the sync engine. This design keeps the watcher agnostic of agent-specific logic while providing the hooks necessary to trigger database updates.
The architecture supports both deep recursive watching and shallow monitoring. For massive directory trees, WatchRecursiveBudgeted (lines 80-124) walks the filesystem and adds each sub-directory to the underlying watch list, tracking root paths for exclusion checks. Conversely, WatchShallow (lines 30-38) registers only the top-level directory, relying on the auto-watch feature to catch dynamically created subdirectories.
Step-by-Step Detection Flow
Initializing the Watcher with NewWatcher
The NewWatcher function accepts three parameters: a debounce duration, an onChange callback, and a slice of exclude patterns. It constructs the underlying fsnotify.Watcher, normalizes exclusion globs (stripping leading "./" and deduplicating entries), and returns a configured watcher ready for directory registration.
// Create a watcher with 200ms debounce, ignoring version control and temp folders
watcher, err := sync.NewWatcher(
200*time.Millisecond,
func(paths []string) {
// This callback receives batched changed paths
engine.SyncPaths(paths)
},
[]string{".git", "node_modules", "**/tmp/**"},
)
Registering Directories: Recursive and Shallow Modes
The watcher provides two registration strategies. WatchRecursive traverses the entire directory tree, while WatchRecursiveBudgeted stops when the OS limit for file descriptors is reached, setting a ResourceExhausted flag for the sync engine to handle via fallback polling. For large but shallow trees, WatchShallow monitors only the specified root, though the watcher will still auto-add subdirectories that belong to recursive roots to avoid shadowing.
The Event Loop and handleEvent
After calling Start(), a dedicated goroutine executes the loop function (lines 55-80). This loop selects across three channels: filesystem events, errors, and stop signals. When fsnotify reports a write, create, remove, or rename operation, handleEvent (lines 83-99) validates the event type and records the affected path in a thread-safe pending map with a timestamp.
Auto-Watching New Subdirectories
When a Create event targets a directory, the watchIfDir helper (lines 105-119) automatically adds it to the monitoring list. This mechanism catches dynamically created session-date folders such as sessions/2024/09/15/ without requiring manual re-registration. The function consults shouldExclude to skip paths matching user-defined glob patterns and avoids adding directories under shallow-only roots.
Debouncing and Batch Flushing
To prevent sync storms during rapid file modifications, the watcher implements a debounce mechanism. A ticker fires at the configured interval, triggering flush (lines 130-165). This function scans the pending map, selects paths whose timestamps exceed the debounce duration, removes them from the map, and invokes the onChange callback with the complete batch of changed paths. This coalesces multiple rapid events into a single database synchronization operation.
Triggering the Sync via Callback
The onChange callback closure is registered by the sync engine in cmd/agentsview/main.go. When invoked, it receives a slice of absolute paths that changed during the debounce window. The engine parses these session files, updates SQLite tables, and optionally pushes changes to PostgreSQL. The watcher itself remains unaware of agent semantics; it purely reports filesystem changes.
Key Design Features
Resource-Aware Recursion: The WatchRecursiveBudgeted method tracks OS file descriptor limits. When exhaustion occurs, it marks the error condition and allows the sync engine to fall back to periodic polling for the remaining subtree.
Exclusion Pattern Matching: Users specify glob-style patterns (e.g., **/tmp/**) that the watcher normalizes and validates via shouldExclude before adding directories to the monitor list.
Thread-Safe Bookkeeping: Two mutexes protect critical state: rootsMu guards the list of watched roots, while mu protects the pending change map. This ensures safe concurrent access between the fsnotify goroutine and the debounce ticker.
Shallow Root Handling: The watcher distinguishes between recursive and shallow roots. When auto-watching new directories, it checks against the shallow root list to prevent over-monitoring massive subtrees that the user explicitly marked as shallow.
Integration Example
The following example demonstrates setting up the watcher to monitor agent session directories and trigger synchronization:
package main
import (
"log"
"time"
"github.com/kenn-io/agentsview/internal/sync"
)
func main() {
// Initialize with 200ms debounce and exclusions
watcher, err := sync.NewWatcher(
200*time.Millisecond,
func(paths []string) {
log.Printf("Syncing %d changed paths", len(paths))
// Engine processes session files and updates SQLite
syncEngine.SyncPaths(paths)
},
[]string{".git", "node_modules", "*.tmp"},
)
if err != nil {
log.Fatal(err)
}
// Watch session directory recursively
err = watcher.WatchRecursive("/home/user/.agentsview/sessions")
if err != nil {
log.Fatal(err)
}
watcher.Start()
defer watcher.Stop()
// Block or run other services...
select {}
}
Summary
- The file watcher mechanism resides in
internal/sync/watcher.goand wraps Go'sfsnotifypackage. NewWatcherinitializes the system with debounce configuration and exclusion patterns.WatchRecursiveandWatchShallowprovide flexible directory monitoring strategies.- The event loop processes filesystem events and populates a pending map with timestamps.
watchIfDirauto-registers newly created subdirectories, enabling dynamic session folder detection.- The
flushmethod debounces rapid changes, batching them into single sync operations. - An
onChangecallback bridges filesystem events to the sync engine incmd/agentsview/main.go. - Thread-safe mutexes and resource exhaustion handling ensure robust production operation.
Frequently Asked Questions
How does the watcher handle newly created session directories?
When the filesystem reports a Create event for a directory, the watchIfDir function (lines 105-119 in internal/sync/watcher.go) automatically adds that directory to the fsnotify watch list. This enables the system to detect agent sessions written to date-organized folders like sessions/2024/09/15/ without requiring manual restarts or re-registration of watch paths.
What happens when the OS file descriptor limit is reached?
The WatchRecursiveBudgeted method tracks the number of directories being watched and stops recursion when the OS limit approaches. It sets a ResourceExhausted flag on the watcher, which the sync engine checks in cmd/agentsview/main.go. The engine then falls back to periodic polling for the unwatched portion of the directory tree, ensuring no sessions are missed while respecting system limits.
How does the debounce mechanism improve performance?
The watcher accumulates filesystem events in a pending map and only invokes the sync callback after the configured debounce interval (typically 200ms) has elapsed without new changes. Implemented in the flush method (lines 130-165), this coalesces rapid bursts of file operations—such as atomic rewrites or batch session imports—into a single database transaction, reducing SQLite lock contention and PostgreSQL sync overhead.
Can the watcher exclude specific directories from monitoring?
Yes. The NewWatcher constructor accepts a slice of glob patterns (e.g., ".git", "node_modules", "**/tmp/**"). These patterns are normalized and stored in the watcher struct. Before adding any directory to the fsnotify watch list, the shouldExclude method checks against these patterns, allowing users to skip version control folders, temporary directories, or large asset caches that would otherwise consume file descriptors and trigger unnecessary sync operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →