From 079effdaebe73c6b11b8037eac332ca40d6e36fe Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 31 Oct 2025 22:01:30 -0700 Subject: [PATCH] Fix bd-373c: Surface daemon errors when multiple .db files exist When daemon detects multiple .db files (after filtering .backup and vc.db), it now writes detailed error to .beads/daemon-error file before exiting. The error file is checked and displayed when: - Daemon discovery fails to connect - Auto-start fails to yield a running daemon - User runs 'bd daemons list' This makes the error immediately visible without requiring users to check daemon logs. Changes: - cmd/bd/daemon.go: Write daemon-error file on multiple .db detection - internal/daemon/discovery.go: Read and surface daemon-error in DaemonInfo.Error - cmd/bd/main.go: Display daemon-error when auto-start fails Amp-Thread-ID: https://ampcode.com/threads/T-1005a8d1-7a5a-4844-ad2d-2b8a6145825f Co-authored-by: Amp --- cmd/bd/daemon.go | 25 ++++++++++++++++++++----- cmd/bd/main.go | 8 ++++++++ internal/daemon/discovery.go | 22 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cmd/bd/daemon.go b/cmd/bd/daemon.go index b9ff0084..49d49f8c 100644 --- a/cmd/bd/daemon.go +++ b/cmd/bd/daemon.go @@ -1371,13 +1371,22 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p } } if len(validDBs) > 1 { - log.log("Error: Multiple database files found in %s:", beadsDir) + errMsg := fmt.Sprintf("Error: Multiple database files found in %s:\n", beadsDir) for _, db := range validDBs { - log.log(" - %s", filepath.Base(db)) + errMsg += fmt.Sprintf(" - %s\n", filepath.Base(db)) } - log.log("") - log.log("Beads requires a single canonical database: %s", beads.CanonicalDatabaseName) - log.log("Run 'bd init' to migrate legacy databases or manually remove old databases") + errMsg += fmt.Sprintf("\nBeads requires a single canonical database: %s\n", beads.CanonicalDatabaseName) + errMsg += "Run 'bd init' to migrate legacy databases or manually remove old databases\n" + errMsg += "Or run 'bd doctor' for more diagnostics" + + log.log(errMsg) + + // Write error to file so user can see it without checking logs + errFile := filepath.Join(beadsDir, "daemon-error") + if err := os.WriteFile(errFile, []byte(errMsg), 0644); err != nil { + log.log("Warning: could not write daemon-error file: %v", err) + } + os.Exit(1) } } @@ -1394,6 +1403,12 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p log.log("Using database: %s", daemonDBPath) + // Clear any previous daemon-error file on successful startup + errFile := filepath.Join(beadsDir, "daemon-error") + if err := os.Remove(errFile); err != nil && !os.IsNotExist(err) { + log.log("Warning: could not remove daemon-error file: %v", err) + } + store, err := sqlite.New(daemonDBPath) if err != nil { log.log("Error: cannot open database: %v", err) diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 029b653a..0565e5c4 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -370,6 +370,14 @@ var rootCmd = &cobra.Command{ if err != nil { daemonStatus.Detail = err.Error() } + // Check for daemon-error file to provide better error message + if beadsDir := filepath.Dir(socketPath); beadsDir != "" { + errFile := filepath.Join(beadsDir, "daemon-error") + if errMsg, readErr := os.ReadFile(errFile); readErr == nil && len(errMsg) > 0 { + fmt.Fprintf(os.Stderr, "\n%s\n", string(errMsg)) + daemonStatus.Detail = string(errMsg) + } + } if os.Getenv("BD_DEBUG") != "" { fmt.Fprintf(os.Stderr, "Debug: auto-start did not yield a running daemon; falling back to direct mode\n") } diff --git a/internal/daemon/discovery.go b/internal/daemon/discovery.go index 2084ac97..a5e12f07 100644 --- a/internal/daemon/discovery.go +++ b/internal/daemon/discovery.go @@ -149,10 +149,18 @@ func discoverDaemon(socketPath string) DaemonInfo { client, err := rpc.TryConnectWithTimeout(socketPath, 500*time.Millisecond) if err != nil { daemon.Error = fmt.Sprintf("failed to connect: %v", err) + // Check for daemon-error file + if errMsg := checkDaemonErrorFile(socketPath); errMsg != "" { + daemon.Error = errMsg + } return daemon } if client == nil { daemon.Error = "daemon not responding or unhealthy" + // Check for daemon-error file + if errMsg := checkDaemonErrorFile(socketPath); errMsg != "" { + daemon.Error = errMsg + } return daemon } defer client.Close() @@ -204,6 +212,20 @@ func FindDaemonByWorkspace(workspacePath string) (*DaemonInfo, error) { return nil, fmt.Errorf("no daemon found for workspace: %s", workspacePath) } +// checkDaemonErrorFile checks for a daemon-error file in the .beads directory +func checkDaemonErrorFile(socketPath string) string { + // Socket path is typically .beads/bd.sock, so get the parent dir + beadsDir := filepath.Dir(socketPath) + errFile := filepath.Join(beadsDir, "daemon-error") + + data, err := os.ReadFile(errFile) + if err != nil { + return "" + } + + return string(data) +} + // CleanupStaleSockets removes socket files and PID files for dead daemons func CleanupStaleSockets(daemons []DaemonInfo) (int, error) { cleaned := 0