diff --git a/.beads/bd.sock.startlock b/.beads/bd.sock.startlock new file mode 100644 index 00000000..d90ab155 --- /dev/null +++ b/.beads/bd.sock.startlock @@ -0,0 +1 @@ +36165 diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 260988da..a8361dba 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -275,7 +275,7 @@ {"id":"bd-iye7","content_hash":"5b39d4322d23d2a38968d81f5100cb438de4cc935f1d6d6be73e8db4be57f1bd","title":"Add path normalization to getMultiRepoJSONLPaths()","description":"From bd-xo6b code review: getMultiRepoJSONLPaths() does not handle non-standard paths correctly.\n\nProblems:\n- No tilde expansion: ~/repos/foo treated as literal path\n- No absolute path conversion: ../other-repo breaks if working directory changes\n- No duplicate detection: If Primary=. and Additional=[.], same JSONL processed twice\n- No empty string handling: Empty paths create invalid /.beads/issues.jsonl\n\nImpact:\nConfig with tilde or relative paths will fail\n\nFix needed:\n1. Use filepath.Abs() for all paths\n2. Add tilde expansion via os.UserHomeDir()\n3. Deduplicate paths (use map to track seen paths)\n4. Filter out empty strings\n5. Validate paths exist and are readable\n\nFiles:\n- cmd/bd/deletion_tracking.go:333-358 (getMultiRepoJSONLPaths function)","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-06T19:31:51.882743-08:00","updated_at":"2025-11-06T19:35:41.246311-08:00","closed_at":"2025-11-06T19:35:41.246311-08:00","source_repo":".","dependencies":[{"issue_id":"bd-iye7","depends_on_id":"bd-xo6b","type":"discovered-from","created_at":"2025-11-06T19:32:12.267906-08:00","created_by":"daemon"}]} {"id":"bd-j0rm","content_hash":"a9d63140e09a3aba769436c54c204e9369912a7d8d51d2aae9e68c8148357d3b","title":"Ready issue","description":"","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-07T19:05:35.35216-08:00","updated_at":"2025-11-07T19:05:35.35216-08:00","source_repo":"."} {"id":"bd-j5aj","content_hash":"2236f911e6f321a74aa61bdf702d24949e44a68ed511d12dd011aa4103c89230","title":"Issue 2","description":"","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-07T19:07:16.364549-08:00","updated_at":"2025-11-07T19:07:16.364549-08:00","source_repo":"."} -{"id":"bd-j7e2","content_hash":"e5cee574c055f80bb884f99f6a5a918f5e7dafa84392f4d4ea6b4b67c406e415","title":"RPC diagnostics: BD_RPC_DEBUG timing logs","description":"Add lightweight diagnostic logging for RPC connection attempts:\n- BD_RPC_DEBUG=1 prints to stderr:\n - Socket path being dialed\n - Socket exists check result \n - Dial start/stop time\n - Connection outcome\n- Improve bd daemon --status messaging when lock not held\n\nThis helps field triage of connection issues without verbose daemon logs.","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-07T16:42:12.772364-08:00","updated_at":"2025-11-07T16:42:12.772364-08:00","source_repo":".","dependencies":[{"issue_id":"bd-j7e2","depends_on_id":"bd-ndyz","type":"discovered-from","created_at":"2025-11-07T16:42:12.773714-08:00","created_by":"daemon"}]} +{"id":"bd-j7e2","content_hash":"aa810a603c630b2435f7a75d4c38a57b72e557e64913c1b701ccda87109f2ffe","title":"RPC diagnostics: BD_RPC_DEBUG timing logs","description":"Add lightweight diagnostic logging for RPC connection attempts:\n- BD_RPC_DEBUG=1 prints to stderr:\n - Socket path being dialed\n - Socket exists check result \n - Dial start/stop time\n - Connection outcome\n- Improve bd daemon --status messaging when lock not held\n\nThis helps field triage of connection issues without verbose daemon logs.","status":"in_progress","priority":3,"issue_type":"task","created_at":"2025-11-07T16:42:12.772364-08:00","updated_at":"2025-11-07T21:26:53.618078-08:00","source_repo":".","dependencies":[{"issue_id":"bd-j7e2","depends_on_id":"bd-ndyz","type":"discovered-from","created_at":"2025-11-07T16:42:12.773714-08:00","created_by":"daemon"}]} {"id":"bd-jpm9","content_hash":"cdd43e0460cfe2e1c0f49728248d4bb441f5c6b17943dd9d13efe32de3e42147","title":"Issue to close","description":"","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-07T19:07:13.57982-08:00","updated_at":"2025-11-07T19:07:13.602394-08:00","closed_at":"2025-11-07T19:07:13.602394-08:00","source_repo":"."} {"id":"bd-jx90","content_hash":"5e08ff79669eaf606022b1ab13a167c0689e9d9a1b2f3bb4fb880ca792546411","title":"Add simple cleanup command to delete closed issues","description":"Users want a simple command to delete all closed issues without requiring Anthropic API key (unlike compact). Requested in GH #243.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-07T00:26:30.372137-08:00","updated_at":"2025-11-07T00:26:30.372137-08:00","source_repo":"."} {"id":"bd-k0j9","content_hash":"52d1e6f87bd7655018bd89dbbbaf8da66bdcba45de6138fd237810365a04606a","title":"Test dependency parent","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T11:23:02.505901-08:00","updated_at":"2025-11-05T11:23:20.91305-08:00","closed_at":"2025-11-05T11:23:20.91305-08:00","source_repo":"."} diff --git a/cmd/bd/daemon_lifecycle.go b/cmd/bd/daemon_lifecycle.go index 3449afda..8bb1f902 100644 --- a/cmd/bd/daemon_lifecycle.go +++ b/cmd/bd/daemon_lifecycle.go @@ -117,7 +117,16 @@ func showDaemonHealth(global bool) { } if client == nil { - fmt.Println("Daemon is not running") + // Check if lock is held to provide better diagnostic message + beadsDir := filepath.Dir(socketPath) + running, _ := tryDaemonLock(beadsDir) + if running { + fmt.Println("Daemon lock is held but connection failed") + fmt.Println("This may indicate a crashed daemon. Try: bd daemons killall") + } else { + fmt.Println("Daemon is not running") + fmt.Println("Start with: bd daemon start") + } os.Exit(1) } defer func() { _ = client.Close() }() diff --git a/internal/rpc/client.go b/internal/rpc/client.go index 5af9bcd0..a2cc9b1e 100644 --- a/internal/rpc/client.go +++ b/internal/rpc/client.go @@ -13,6 +13,19 @@ import ( "github.com/steveyegge/beads/internal/lockfile" ) +// rpcDebugEnabled returns true if BD_RPC_DEBUG environment variable is set +func rpcDebugEnabled() bool { + val := os.Getenv("BD_RPC_DEBUG") + return val == "1" || val == "true" +} + +// rpcDebugLog logs to stderr if BD_RPC_DEBUG is enabled +func rpcDebugLog(format string, args ...interface{}) { + if rpcDebugEnabled() { + fmt.Fprintf(os.Stderr, "[RPC DEBUG] "+format+"\n", args...) + } +} + // ClientVersion is the version of this RPC client // This should match the bd CLI version for proper compatibility checks // It's set dynamically by main.go from cmd/bd/version.go before making RPC calls @@ -35,35 +48,49 @@ func TryConnect(socketPath string) (*Client, error) { // TryConnectWithTimeout attempts to connect to the daemon socket using the provided dial timeout. // Returns nil if no daemon is running or unhealthy. func TryConnectWithTimeout(socketPath string, dialTimeout time.Duration) (*Client, error) { + rpcDebugLog("attempting connection to socket: %s", socketPath) + // Fast probe: check daemon lock before attempting RPC connection if socket doesn't exist // This eliminates unnecessary connection attempts when no daemon is running // If socket exists, we skip lock check for backwards compatibility and test scenarios socketExists := endpointExists(socketPath) + rpcDebugLog("socket exists check: %v", socketExists) + if !socketExists { beadsDir := filepath.Dir(socketPath) running, _ := lockfile.TryDaemonLock(beadsDir) if !running { debug.Logf("daemon lock not held and socket missing (no daemon running)") + rpcDebugLog("daemon lock not held (no daemon running)") // Self-heal: clean up stale artifacts when lock is free and socket is missing cleanupStaleDaemonArtifacts(beadsDir) return nil, nil } + rpcDebugLog("daemon lock held but socket missing (race or cleanup issue)") } if !socketExists { debug.Logf("RPC endpoint does not exist: %s", socketPath) + rpcDebugLog("connection aborted: socket does not exist") return nil, nil } if dialTimeout <= 0 { dialTimeout = 200 * time.Millisecond } - + + rpcDebugLog("dialing socket (timeout: %v)", dialTimeout) + dialStart := time.Now() conn, err := dialRPC(socketPath, dialTimeout) + dialDuration := time.Since(dialStart) + if err != nil { debug.Logf("failed to connect to RPC endpoint: %v", err) + rpcDebugLog("dial failed after %v: %v", dialDuration, err) return nil, nil } + + rpcDebugLog("dial succeeded in %v", dialDuration) client := &Client{ conn: conn, @@ -71,21 +98,29 @@ func TryConnectWithTimeout(socketPath string, dialTimeout time.Duration) (*Clien timeout: 30 * time.Second, } + rpcDebugLog("performing health check") + healthStart := time.Now() health, err := client.Health() + healthDuration := time.Since(healthStart) + if err != nil { debug.Logf("health check failed: %v", err) + rpcDebugLog("health check failed after %v: %v", healthDuration, err) _ = conn.Close() return nil, nil } if health.Status == "unhealthy" { debug.Logf("daemon unhealthy: %s", health.Error) + rpcDebugLog("daemon unhealthy (checked in %v): %s", healthDuration, health.Error) _ = conn.Close() return nil, nil } debug.Logf("connected to daemon (status: %s, uptime: %.1fs)", health.Status, health.Uptime) + rpcDebugLog("connection successful (health check: %v, status: %s, uptime: %.1fs)", + healthDuration, health.Status, health.Uptime) return client, nil }