From 195091711e84bba886b67af1bc79df8954ec15e1 Mon Sep 17 00:00:00 2001 From: "Charles P. Cross" Date: Mon, 22 Dec 2025 19:02:42 -0500 Subject: [PATCH] fix(daemon): deduplicate file and git ref change log messages The daemon.log was showing duplicate log messages: - 'File change detected' appeared 4x for a single file change - 'Git ref change detected' appeared 2x for a single ref update Root cause: - fsnotify generates multiple events (Write, Create, Chmod) for single file ops - Both parent directory and file watchers can trigger for the same change - Multiple git ref files may be updated simultaneously Fix: - Add log deduplication with 500ms window (matching debouncer window) - Track last log time for file changes and git ref changes separately - Only log if enough time has passed since last log of same type - Still trigger debouncer for every event (functionality unchanged) This reduces log noise while maintaining full functionality. --- cmd/bd/daemon_watcher.go | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/cmd/bd/daemon_watcher.go b/cmd/bd/daemon_watcher.go index 56b5f09e..5075b791 100644 --- a/cmd/bd/daemon_watcher.go +++ b/cmd/bd/daemon_watcher.go @@ -30,6 +30,11 @@ type FileWatcher struct { lastHeadExists bool cancel context.CancelFunc wg sync.WaitGroup // Track goroutines for graceful shutdown (bd-jo38) + // Log deduplication: track last log times to avoid duplicate messages + lastFileLogTime time.Time + lastGitRefLogTime time.Time + logDedupeWindow time.Duration + logMu sync.Mutex } // NewFileWatcher creates a file watcher for the given JSONL path. @@ -37,10 +42,11 @@ type FileWatcher struct { // Falls back to polling mode if fsnotify fails (controlled by BEADS_WATCHER_FALLBACK env var). func NewFileWatcher(jsonlPath string, onChanged func()) (*FileWatcher, error) { fw := &FileWatcher{ - jsonlPath: jsonlPath, - parentDir: filepath.Dir(jsonlPath), - debouncer: NewDebouncer(500*time.Millisecond, onChanged), - pollInterval: 5 * time.Second, + jsonlPath: jsonlPath, + parentDir: filepath.Dir(jsonlPath), + debouncer: NewDebouncer(500*time.Millisecond, onChanged), + pollInterval: 5 * time.Second, + logDedupeWindow: 500 * time.Millisecond, // Deduplicate logs within this window } // Get initial file state for polling fallback @@ -120,6 +126,30 @@ func NewFileWatcher(jsonlPath string, onChanged func()) (*FileWatcher, error) { return fw, nil } +// shouldLogFileChange returns true if enough time has passed since last file change log +func (fw *FileWatcher) shouldLogFileChange() bool { + fw.logMu.Lock() + defer fw.logMu.Unlock() + now := time.Now() + if now.Sub(fw.lastFileLogTime) >= fw.logDedupeWindow { + fw.lastFileLogTime = now + return true + } + return false +} + +// shouldLogGitRefChange returns true if enough time has passed since last git ref change log +func (fw *FileWatcher) shouldLogGitRefChange() bool { + fw.logMu.Lock() + defer fw.logMu.Unlock() + now := time.Now() + if now.Sub(fw.lastGitRefLogTime) >= fw.logDedupeWindow { + fw.lastGitRefLogTime = now + return true + } + return false +} + // Start begins monitoring filesystem events or polling. // Runs in background goroutine until context is canceled. // Should only be called once per FileWatcher instance. @@ -156,7 +186,9 @@ func (fw *FileWatcher) Start(ctx context.Context, log daemonLogger) { // Handle JSONL write/chmod events if event.Name == fw.jsonlPath && event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Chmod) != 0 { - log.log("File change detected: %s (op: %v)", event.Name, event.Op) + if fw.shouldLogFileChange() { + log.log("File change detected: %s", event.Name) + } fw.debouncer.Trigger() continue } @@ -179,7 +211,9 @@ func (fw *FileWatcher) Start(ctx context.Context, log daemonLogger) { // Handle git ref changes (only events under gitRefsPath) if event.Op&fsnotify.Write != 0 && strings.HasPrefix(event.Name, fw.gitRefsPath) { - log.log("Git ref change detected: %s", event.Name) + if fw.shouldLogGitRefChange() { + log.log("Git ref change detected: %s", event.Name) + } fw.debouncer.Trigger() continue }