From 349b892123247148b8ecab5168259702daf12e65 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 31 Oct 2025 20:18:05 -0700 Subject: [PATCH] Harden event-driven daemon for production Three critical fixes to make event-driven mode production-ready: 1. Skip redundant imports: Check JSONL mtime vs DB mtime to avoid self-triggered import loops after export writes JSONL 2. Add server.Stop() in serverErrChan case: Ensures clean RPC server shutdown on errors 3. Fallback ticker (60s): When file watcher unavailable (e.g., network filesystems), fall back to periodic polling to detect remote changes These minimal fixes address Oracle's concerns without over-engineering. Event-driven mode is now safe for default. Amp-Thread-ID: https://ampcode.com/threads/T-a9a67394-37ca-4b79-aa23-c5c011f9c0cd Co-authored-by: Amp --- cmd/bd/daemon.go | 36 +++++++++++++++++++++++++++++------- cmd/bd/daemon_event_loop.go | 31 ++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/cmd/bd/daemon.go b/cmd/bd/daemon.go index 5ff1e390..5d63e6e8 100644 --- a/cmd/bd/daemon.go +++ b/cmd/bd/daemon.go @@ -1112,14 +1112,36 @@ func createAutoImportFunc(ctx context.Context, store storage.Storage, log daemon log.log("Removed stale lock (%s), proceeding", holder) } - // Pull from git - if err := gitPull(importCtx); err != nil { - log.log("Pull failed: %v", err) - return - } - log.log("Pulled from remote") + // Check JSONL modification time to avoid redundant imports + // (e.g., from self-triggered file watcher events after our own export) + jsonlInfo, err := os.Stat(jsonlPath) + if err != nil { + log.log("Failed to stat JSONL: %v", err) + return + } - // Count issues before import + // Get database modification time + dbPath := filepath.Join(beadsDir, "beads.db") + dbInfo, err := os.Stat(dbPath) + if err != nil { + log.log("Failed to stat database: %v", err) + return + } + + // Skip if JSONL is older than database (nothing new to import) + if !jsonlInfo.ModTime().After(dbInfo.ModTime()) { + log.log("Skipping import: JSONL not newer than database") + return + } + + // Pull from git + if err := gitPull(importCtx); err != nil { + log.log("Pull failed: %v", err) + return + } + log.log("Pulled from remote") + + // Count issues before import beforeCount, err := countDBIssues(importCtx, store) if err != nil { log.log("Failed to count issues before import: %v", err) diff --git a/cmd/bd/daemon_event_loop.go b/cmd/bd/daemon_event_loop.go index 9c33b18c..4ec2b4ca 100644 --- a/cmd/bd/daemon_event_loop.go +++ b/cmd/bd/daemon_event_loop.go @@ -47,9 +47,13 @@ func runEventDrivenLoop( watcher, err := NewFileWatcher(jsonlPath, func() { importDebouncer.Trigger() }) + var fallbackTicker *time.Ticker if err != nil { - log.log("WARNING: File watcher unavailable (%v), mutations will trigger export only", err) + log.log("WARNING: File watcher unavailable (%v), using 60s polling fallback", err) watcher = nil + // Fallback ticker to check for remote changes when watcher unavailable + fallbackTicker = time.NewTicker(60 * time.Second) + defer fallbackTicker.Stop() } else { watcher.Start(ctx, log) defer watcher.Close() @@ -97,6 +101,16 @@ func runEventDrivenLoop( // Periodic health validation (not sync) checkDaemonHealth(ctx, store, log) + case <-func() <-chan time.Time { + if fallbackTicker != nil { + return fallbackTicker.C + } + // Never fire if watcher is available + return make(chan time.Time) + }(): + log.log("Fallback ticker: checking for remote changes") + importDebouncer.Trigger() + case sig := <-sigChan: if isReloadSignal(sig) { log.log("Received reload signal, ignoring") @@ -120,12 +134,15 @@ func runEventDrivenLoop( return case err := <-serverErrChan: - log.log("RPC server failed: %v", err) - cancel() - if watcher != nil { - watcher.Close() - } - return + log.log("RPC server failed: %v", err) + cancel() + if watcher != nil { + watcher.Close() + } + if stopErr := server.Stop(); stopErr != nil { + log.log("Error stopping server: %v", stopErr) + } + return } } }