From 8f80dde0ad1ecce708336a441a0d1eda5df27018 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 18 Oct 2025 16:09:55 -0700 Subject: [PATCH] Add global daemon auto-start support (bd-149) - Implement shouldUseGlobalDaemon() with multi-repo detection - Auto-detect 4+ beads repos and prefer global daemon - Support BEADS_PREFER_GLOBAL_DAEMON env var for explicit control - Add 'bd daemon --migrate-to-global' migration helper - Update auto-start logic to use global daemon when appropriate - Update documentation in AGENTS.md and README.md Amp-Thread-ID: https://ampcode.com/threads/T-9af9372d-f3f3-4698-920d-e5ad1486d849 Co-authored-by: Amp --- .beads/issues.jsonl | 4 +- AGENTS.md | 12 +++++- README.md | 13 +++++- cmd/bd/daemon.go | 74 +++++++++++++++++++++++++++++++ cmd/bd/main.go | 103 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 5 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 62fa4b33..2a2d43a6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -52,8 +52,8 @@ {"id":"bd-145","title":"Add storage cache eviction policy to daemon","description":"Daemon caches DB connections forever in storageCache map (server.go:29). For users with 50+ repos, this causes memory leaks and file descriptor exhaustion.\n\nNeed LRU cache with:\n- Max size limit (default: 50 repos)\n- TTL-based eviction (default: 30min idle)\n- Periodic cleanup goroutine\n\nLocation: internal/rpc/server.go:29-40","design":"Add StorageCacheEntry struct with lastAccess timestamp.\n\nImplement evictStaleStorage() method that runs every 5 minutes to close connections idle \u003e30min.\n\nAdd max cache size enforcement (LRU eviction when full).\n\nMake limits configurable via env vars:\n- BEADS_DAEMON_MAX_CACHE_SIZE (default: 50)\n- BEADS_DAEMON_CACHE_TTL (default: 30m)","acceptance_criteria":"- Cache evicts entries after 30min idle\n- Cache respects max size limit\n- Cleanup goroutine runs periodically\n- Evicted storage connections are properly closed\n- No resource leaks under sustained load\n- Unit tests for eviction logic","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-10-18T13:05:46.174245-07:00","updated_at":"2025-10-18T13:16:56.921023-07:00","closed_at":"2025-10-18T13:16:56.921023-07:00","dependencies":[{"issue_id":"bd-145","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.077954-07:00","created_by":"daemon"}]} {"id":"bd-146","title":"Add daemon health check endpoint and probes","description":"Auto-start only checks socket existence, not daemon responsiveness. Daemon can be running but unresponsive (deadlock, hung DB). Users work in degraded direct mode without knowing why.\n\nNeed health check RPC operation that:\n- Tests DB connectivity (1s timeout)\n- Returns uptime, status, metrics\n- Used by auto-start before connecting\n- Enables monitoring/alerting\n\nLocation: internal/rpc/server.go, cmd/bd/main.go:100-108","design":"Add OpHealth RPC operation to protocol.\n\nhandleHealth() implementation:\n- Quick DB ping with 1s timeout\n- Return status, uptime, version\n- Include basic metrics (connections, cache size)\n\nUpdate TryConnect() to call Health() after socket connection:\n- If health check fails, close connection and return nil\n- Enables transparent failover to direct mode\n\nAdd 'bd daemon --health' CLI command for monitoring.","acceptance_criteria":"- Health check RPC endpoint works\n- Returns structured health status\n- Client uses health check before operations\n- bd daemon --health command exists\n- Unhealthy daemon triggers auto-restart or fallback\n- Health check completes in \u003c2 seconds","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-10-18T13:05:58.647592-07:00","updated_at":"2025-10-18T13:32:15.106003-07:00","closed_at":"2025-10-18T13:32:15.106003-07:00","dependencies":[{"issue_id":"bd-146","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.093618-07:00","created_by":"daemon"}]} {"id":"bd-147","title":"Add stale socket and crash recovery for daemon","description":"When daemon crashes (panic, OOM, signal), socket file remains and blocks new daemon start. Users must manually remove .beads/bd.sock.\n\nProblems:\n- Socket file remains after crash\n- PID file remains (isDaemonRunning false positive)\n- No automatic recovery\n- Users get 'daemon already running' error\n\nLocation: cmd/bd/daemon.go, cmd/bd/main.go:221-311","design":"Improve stale detection in tryAutoStartDaemon():\n\n1. If socket exists, try to connect\n2. If connection fails → stale socket, remove it\n3. Also remove PID file and lock files\n4. Retry daemon start\n\nAdd self-healing to daemon startup:\n- On startup, check for stale PID files\n- If PID in file doesn't exist, remove and continue\n- Use exclusive file lock to prevent races\n\nOptional: Add crash recovery watchdog that restarts daemon on exit.","acceptance_criteria":"- Stale sockets are automatically detected and removed\n- Auto-start recovers from daemon crashes\n- No manual intervention needed for crash recovery\n- PID file management is robust\n- Lock files prevent multiple daemon instances\n- Tests for crash recovery scenarios","notes":"Oracle code review identified race conditions. Improvements made:\n- Moved cleanup AFTER lock acquisition (prevents unlinking live sockets)\n- Added PID liveness check before removing socket\n- Added stale lock detection with retry\n- Tightened directory permissions to 0700\n- Improved socket readiness probing with short timeouts\n- Made removeOldSocket() ignore ENOENT errors\n\nChanges eliminate race where socket could be removed during daemon startup window.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-10-18T13:06:10.116917-07:00","updated_at":"2025-10-18T13:56:51.318675-07:00","closed_at":"2025-10-18T13:56:51.318675-07:00","dependencies":[{"issue_id":"bd-147","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.108099-07:00","created_by":"daemon"}]} -{"id":"bd-148","title":"Add lifecycle management for beads-mcp processes","description":"MCP server processes accumulate without cleanup. Each tool invocation spawns a new Python process that lingers after Claude disconnects.\n\nObserved: 6+ beads-mcp processes running simultaneously.\n\nProblems:\n- No parent-child relationship tracking\n- No cleanup on MCP client disconnect\n- Processes leak over days of use\n- Could accumulate hundreds of processes\n\nLocation: integrations/beads-mcp/src/beads_mcp/server.py","design":"Add proper cleanup handlers to MCP server:\n\n1. Register atexit handler to close daemon connections\n2. Handle SIGTERM/SIGINT for graceful shutdown\n3. Close daemon client in cleanup()\n4. Remove any temp files\n\nOptional improvements:\n- Track active connections to daemon\n- Implement connection pooling\n- Add process timeout/TTL\n- Log lifecycle events for debugging\n\nExample:\nimport atexit\nimport signal\n\ndef cleanup():\n # Close daemon connections\n # Remove temp files\n pass\n\natexit.register(cleanup)\nsignal.signal(signal.SIGTERM, lambda s, f: cleanup())","acceptance_criteria":"- MCP processes clean up on exit\n- Daemon connections are properly closed\n- No process leaks after repeated use\n- Signal handlers work correctly\n- Cleanup runs on normal and abnormal exit\n- Test with multiple concurrent MCP invocations","status":"open","priority":0,"issue_type":"bug","created_at":"2025-10-18T13:06:22.030027-07:00","updated_at":"2025-10-18T13:06:22.030027-07:00","dependencies":[{"issue_id":"bd-148","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.121494-07:00","created_by":"daemon"}]} -{"id":"bd-149","title":"Add global daemon auto-start support","description":"Auto-start only works for local daemon. Users with multiple repos must manually run 'bd daemon --global'.\n\nProblems:\n- No detection of whether global daemon is preferable\n- No migration path from local → global\n- Multi-repo users don't discover global daemon\n- Manual setup required\n\nLocation: cmd/bd/main.go:221-311","design":"Add heuristics to shouldUseGlobalDaemon():\n\n1. Count .beads repos under home directory\n2. If \u003e3 repos found, prefer global daemon\n3. Check BEADS_PREFER_GLOBAL_DAEMON env var\n4. Check config file preference\n\nUpdate tryAutoStartDaemon() to:\n- Use shouldUseGlobalDaemon() to pick mode\n- Pass --global flag when appropriate\n- Log decision for debugging\n\nAdd migration helper:\n- Detect running local daemon\n- Suggest switching to global if multi-repo detected\n- bd daemon --migrate-to-global command","acceptance_criteria":"- Auto-start uses global daemon when appropriate\n- Multi-repo detection works correctly\n- Users can configure preference\n- Migration path is smooth\n- Both local and global auto-start work\n- Documentation updated","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-18T13:06:33.633238-07:00","updated_at":"2025-10-18T13:06:33.633238-07:00","dependencies":[{"issue_id":"bd-149","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.135552-07:00","created_by":"daemon"}]} +{"id":"bd-148","title":"Add lifecycle management for beads-mcp processes","description":"MCP server processes accumulate without cleanup. Each tool invocation spawns a new Python process that lingers after Claude disconnects.\n\nObserved: 6+ beads-mcp processes running simultaneously.\n\nProblems:\n- No parent-child relationship tracking\n- No cleanup on MCP client disconnect\n- Processes leak over days of use\n- Could accumulate hundreds of processes\n\nLocation: integrations/beads-mcp/src/beads_mcp/server.py","design":"Add proper cleanup handlers to MCP server:\n\n1. Register atexit handler to close daemon connections\n2. Handle SIGTERM/SIGINT for graceful shutdown\n3. Close daemon client in cleanup()\n4. Remove any temp files\n\nOptional improvements:\n- Track active connections to daemon\n- Implement connection pooling\n- Add process timeout/TTL\n- Log lifecycle events for debugging\n\nExample:\nimport atexit\nimport signal\n\ndef cleanup():\n # Close daemon connections\n # Remove temp files\n pass\n\natexit.register(cleanup)\nsignal.signal(signal.SIGTERM, lambda s, f: cleanup())","acceptance_criteria":"- MCP processes clean up on exit\n- Daemon connections are properly closed\n- No process leaks after repeated use\n- Signal handlers work correctly\n- Cleanup runs on normal and abnormal exit\n- Test with multiple concurrent MCP invocations","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-10-18T13:06:22.030027-07:00","updated_at":"2025-10-18T14:27:41.87646-07:00","closed_at":"2025-10-18T14:27:41.87646-07:00","dependencies":[{"issue_id":"bd-148","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.121494-07:00","created_by":"daemon"}]} +{"id":"bd-149","title":"Add global daemon auto-start support","description":"Auto-start only works for local daemon. Users with multiple repos must manually run 'bd daemon --global'.\n\nProblems:\n- No detection of whether global daemon is preferable\n- No migration path from local → global\n- Multi-repo users don't discover global daemon\n- Manual setup required\n\nLocation: cmd/bd/main.go:221-311","design":"Add heuristics to shouldUseGlobalDaemon():\n\n1. Count .beads repos under home directory\n2. If \u003e3 repos found, prefer global daemon\n3. Check BEADS_PREFER_GLOBAL_DAEMON env var\n4. Check config file preference\n\nUpdate tryAutoStartDaemon() to:\n- Use shouldUseGlobalDaemon() to pick mode\n- Pass --global flag when appropriate\n- Log decision for debugging\n\nAdd migration helper:\n- Detect running local daemon\n- Suggest switching to global if multi-repo detected\n- bd daemon --migrate-to-global command","acceptance_criteria":"- Auto-start uses global daemon when appropriate\n- Multi-repo detection works correctly\n- Users can configure preference\n- Migration path is smooth\n- Both local and global auto-start work\n- Documentation updated","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-10-18T13:06:33.633238-07:00","updated_at":"2025-10-18T14:41:28.594337-07:00","closed_at":"2025-10-18T14:41:28.594337-07:00","dependencies":[{"issue_id":"bd-149","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.135552-07:00","created_by":"daemon"}]} {"id":"bd-15","title":"Add performance benchmarks document","description":"Document actual performance metrics with hyperfine tests","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-16T20:46:08.971822-07:00","updated_at":"2025-10-18T10:09:23.532938-07:00","closed_at":"2025-10-18T10:09:23.532938-07:00","dependencies":[{"issue_id":"bd-15","depends_on_id":"bd-1","type":"parent-child","created_at":"2025-10-16T21:51:08.918095-07:00","created_by":"renumber"}]} {"id":"bd-150","title":"Improve daemon fallback visibility and user feedback","description":"When daemon is unavailable, bd silently falls back to direct mode. Users don't know:\n- That daemon exists\n- Why auto-start failed\n- That they're in degraded mode\n- How to fix it\n\nThis creates confusion for multi-repo users who get slower performance without explanation.\n\nLocation: cmd/bd/main.go:98-130","design":"Add visibility at multiple levels:\n\n1. Debug logging (existing BD_DEBUG):\n - Already shows daemon connection attempts\n - Add auto-start success/failure\n\n2. Verbose mode (BD_VERBOSE):\n - Show warning when falling back\n - Suggest 'bd daemon --status' to check\n\n3. Status indicator:\n - Add daemon status to all commands when --json\n - Example: {\"daemon_status\": \"healthy\", \"daemon_type\": \"local\", ...}\n\n4. Explicit status command:\n - bd daemon --status shows detailed info\n - Shows whether daemon is running/healthy/unavailable\n\n5. Helpful error messages:\n - When auto-start fails repeatedly\n - When falling back after health check failure\n - With actionable next steps","acceptance_criteria":"- Users can see daemon status easily\n- Fallback warnings are helpful not noisy\n- JSON output includes daemon status\n- Error messages are actionable\n- Documentation explains status indicators\n- bd daemon --status command works","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-18T13:06:46.212558-07:00","updated_at":"2025-10-18T13:06:46.212558-07:00","dependencies":[{"issue_id":"bd-150","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.147701-07:00","created_by":"daemon"}]} {"id":"bd-151","title":"Add version compatibility checks for daemon RPC protocol","description":"Client (bd CLI) and daemon may be different versions after upgrade. This causes:\n- Missing features (newer CLI, older daemon)\n- Protocol mismatches (older CLI, newer daemon)\n- Silent failures or confusing errors\n- No guidance to restart daemon\n\nLocation: internal/rpc/protocol.go, internal/rpc/client.go","design":"Add version field to RPC protocol:\n\n1. Add ClientVersion to Request struct\n2. Populate from Version constant in client\n3. Server checks compatibility in handleRequest()\n\nCompatibility rules:\n- Major version must match\n- Minor version backward compatible\n- Patch version always compatible\n\nOn mismatch:\n- Return clear error message\n- Suggest 'bd daemon --stop \u0026\u0026 bd daemon'\n- Log version info for debugging\n\nAdd to ping/health response:\n- Server version\n- Protocol version\n- Compatibility info\n\nAdd bd version --daemon command to check running daemon version.","acceptance_criteria":"- Version field in RPC protocol\n- Server validates client version\n- Clear error messages on mismatch\n- Health check returns version info\n- bd version --daemon command works\n- Documentation on version policy\n- Tests for version compatibility","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-18T13:06:57.417411-07:00","updated_at":"2025-10-18T13:06:57.417411-07:00","dependencies":[{"issue_id":"bd-151","depends_on_id":"bd-155","type":"parent-child","created_at":"2025-10-18T13:07:49.158023-07:00","created_by":"daemon"}]} diff --git a/AGENTS.md b/AGENTS.md index acb9f34c..63e1d257 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,7 @@ The MCP server will: 2. Fall back to global daemon socket (`~/.beads/bd.sock`) 3. Automatically route requests to the correct database based on your current working directory 4. Auto-start the daemon if it's not running (with exponential backoff on failures) +5. Auto-detect multiple repositories and prefer global daemon when 4+ repos are found **Option 2: Multiple MCP Server Instances** Configure separate MCP servers for each major project: @@ -93,10 +94,19 @@ Configure separate MCP servers for each major project: Each MCP instance will use its specified working directory to find the correct `.beads/*.db` database. **Which approach should you use?** -- ✅ **Global daemon**: Best for 3+ projects, better resource usage, automatic routing +- ✅ **Global daemon**: Best for 3+ projects, better resource usage, automatic routing (auto-started when 4+ repos detected) - ✅ **Multiple instances**: Best for 1-2 projects you switch between frequently - ✅ **Hybrid**: Run global daemon + use MCP instances for convenience +**Migration helper:** +```bash +# Migrate from local to global daemon +bd daemon --migrate-to-global + +# Or set environment variable for permanent preference +export BEADS_PREFER_GLOBAL_DAEMON=1 +``` + ### CLI Quick Reference If you're not using the MCP server, here are the CLI commands: diff --git a/README.md b/README.md index e745a8d3..4bac751a 100644 --- a/README.md +++ b/README.md @@ -926,6 +926,7 @@ bd daemon --log /var/log/bd.log # Custom log file path bd daemon --status # Show daemon status bd daemon --stop # Stop running daemon bd daemon --global # Run as global daemon (see below) +bd daemon --migrate-to-global # Migrate from local to global daemon ``` The daemon is ideal for: @@ -949,6 +950,9 @@ bd daemon --status --global # Stop global daemon bd daemon --stop --global + +# Migrate from local to global daemon (automatically stops local, starts global) +bd daemon --migrate-to-global ``` **Local vs Global Daemon:** @@ -967,8 +971,13 @@ bd daemon --stop --global **How it works:** 1. Global daemon creates socket at `~/.beads/bd.sock` 2. CLI commands check local socket first, then fall back to global -3. Daemon serves requests from any repository -4. Each repo still has its own database at `.beads/*.db` +3. Daemon auto-starts globally when 4+ beads repos are detected +4. Daemon serves requests from any repository +5. Each repo still has its own database at `.beads/*.db` + +**Auto-start global daemon:** +- Set `BEADS_PREFER_GLOBAL_DAEMON=1` to always prefer global daemon +- Or let bd automatically detect when global daemon is appropriate (4+ repos) **Architecture:** ```mermaid diff --git a/cmd/bd/daemon.go b/cmd/bd/daemon.go index b9f7022e..becaa1fc 100644 --- a/cmd/bd/daemon.go +++ b/cmd/bd/daemon.go @@ -44,6 +44,7 @@ Use --health to check daemon health and metrics.`, stop, _ := cmd.Flags().GetBool("stop") status, _ := cmd.Flags().GetBool("status") health, _ := cmd.Flags().GetBool("health") + migrateToGlobal, _ := cmd.Flags().GetBool("migrate-to-global") interval, _ := cmd.Flags().GetDuration("interval") autoCommit, _ := cmd.Flags().GetBool("auto-commit") autoPush, _ := cmd.Flags().GetBool("auto-push") @@ -71,6 +72,11 @@ Use --health to check daemon health and metrics.`, return } + if migrateToGlobal { + migrateToGlobalDaemon() + return + } + if stop { stopDaemon(pidFile) return @@ -127,6 +133,7 @@ func init() { daemonCmd.Flags().Bool("stop", false, "Stop running daemon") daemonCmd.Flags().Bool("status", false, "Show daemon status") daemonCmd.Flags().Bool("health", false, "Check daemon health and metrics") + daemonCmd.Flags().Bool("migrate-to-global", false, "Migrate from local to global daemon") daemonCmd.Flags().String("log", "", "Log file path (default: .beads/daemon.log)") daemonCmd.Flags().Bool("global", false, "Run as global daemon (socket at ~/.beads/bd.sock)") rootCmd.AddCommand(daemonCmd) @@ -330,6 +337,73 @@ func showDaemonHealth(global bool) { } } +func migrateToGlobalDaemon() { + home, err := os.UserHomeDir() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: cannot get home directory: %v\n", err) + os.Exit(1) + } + + localPIDFile := filepath.Join(".beads", "daemon.pid") + globalPIDFile := filepath.Join(home, ".beads", "daemon.pid") + + // Check if local daemon is running + localRunning, localPID := isDaemonRunning(localPIDFile) + if !localRunning { + fmt.Println("No local daemon is running") + } else { + fmt.Printf("Stopping local daemon (PID %d)...\n", localPID) + stopDaemon(localPIDFile) + } + + // Check if global daemon is already running + globalRunning, globalPID := isDaemonRunning(globalPIDFile) + if globalRunning { + fmt.Printf("✓ Global daemon already running (PID %d)\n", globalPID) + return + } + + // Start global daemon + fmt.Println("Starting global daemon...") + binPath, err := os.Executable() + if err != nil { + binPath = os.Args[0] + } + + cmd := exec.Command(binPath, "daemon", "--global") + devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0) + if err == nil { + cmd.Stdout = devNull + cmd.Stderr = devNull + cmd.Stdin = devNull + defer devNull.Close() + } + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to start global daemon: %v\n", err) + os.Exit(1) + } + + go cmd.Wait() + + // Wait for daemon to be ready + time.Sleep(2 * time.Second) + + if isRunning, pid := isDaemonRunning(globalPIDFile); isRunning { + fmt.Printf("✓ Global daemon started successfully (PID %d)\n", pid) + fmt.Println() + fmt.Println("Migration complete! The global daemon will now serve all your beads repositories.") + fmt.Println("Set BEADS_PREFER_GLOBAL_DAEMON=1 in your shell to make this permanent.") + } else { + fmt.Fprintf(os.Stderr, "Error: global daemon failed to start\n") + os.Exit(1) + } +} + func stopDaemon(pidFile string) { if isRunning, pid := isDaemonRunning(pidFile); !isRunning { fmt.Println("Daemon is not running") diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 7f8c93c3..f0b37938 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -219,6 +219,100 @@ func shouldAutoStartDaemon() bool { return true // Default to enabled } +// shouldUseGlobalDaemon determines if global daemon should be preferred +// based on environment variables, config, or heuristics (multi-repo detection) +func shouldUseGlobalDaemon() bool { + // Check explicit environment variable first + if pref := os.Getenv("BEADS_PREFER_GLOBAL_DAEMON"); pref != "" { + return pref == "1" || strings.ToLower(pref) == "true" + } + + // Heuristic: detect multiple beads repositories + home, err := os.UserHomeDir() + if err != nil { + return false + } + + // Count .beads directories under home + repoCount := 0 + maxDepth := 5 // Don't scan too deep + + var countRepos func(string, int) error + countRepos = func(dir string, depth int) error { + if depth > maxDepth || repoCount > 3 { + return filepath.SkipDir + } + + entries, err := os.ReadDir(dir) + if err != nil { + return nil // Skip directories we can't read + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + name := entry.Name() + + // Skip hidden dirs except .beads + if strings.HasPrefix(name, ".") && name != ".beads" { + continue + } + + // Skip common large directories + if name == "node_modules" || name == "vendor" || name == "target" || name == ".git" { + continue + } + + path := filepath.Join(dir, name) + + // Check if this is a .beads directory with a database + if name == ".beads" { + dbPath := filepath.Join(path, "db.sqlite") + if _, err := os.Stat(dbPath); err == nil { + repoCount++ + if repoCount > 3 { + return filepath.SkipDir + } + } + continue + } + + // Recurse into subdirectories + if depth < maxDepth { + countRepos(path, depth+1) + } + } + return nil + } + + // Scan common project directories + projectDirs := []string{ + filepath.Join(home, "src"), + filepath.Join(home, "projects"), + filepath.Join(home, "code"), + filepath.Join(home, "workspace"), + filepath.Join(home, "dev"), + } + + for _, dir := range projectDirs { + if _, err := os.Stat(dir); err == nil { + countRepos(dir, 0) + if repoCount > 3 { + break + } + } + } + + if os.Getenv("BD_DEBUG") != "" { + fmt.Fprintf(os.Stderr, "Debug: found %d beads repositories, prefer global: %v\n", repoCount, repoCount > 3) + } + + // Use global daemon if we found more than 3 repositories + return repoCount > 3 +} + // tryAutoStartDaemon attempts to start the daemon in the background // Returns true if daemon was started successfully and socket is ready func tryAutoStartDaemon(socketPath string) bool { @@ -305,11 +399,20 @@ func tryAutoStartDaemon(socketPath string) bool { } // Determine if we should start global or local daemon + // If requesting local socket, check if we should suggest global instead isGlobal := false if home, err := os.UserHomeDir(); err == nil { globalSocket := filepath.Join(home, ".beads", "bd.sock") if socketPath == globalSocket { isGlobal = true + } else if shouldUseGlobalDaemon() { + // User has multiple repos, but requested local daemon + // Auto-start global daemon instead and log suggestion + isGlobal = true + socketPath = globalSocket + if os.Getenv("BD_DEBUG") != "" { + fmt.Fprintf(os.Stderr, "Debug: detected multiple repos, auto-starting global daemon\n") + } } }