From 433115725bb5b6f32ad2fcccb7cf4d523b0467e2 Mon Sep 17 00:00:00 2001 From: emma Date: Fri, 23 Jan 2026 20:20:59 -0800 Subject: [PATCH] fix(dolt): server mode should support multi-process access Code review fix: In server mode, Dolt connects to an external sql-server and should NOT be single-process-only. The whole point of server mode is to enable multi-writer access. Changes: - Add Config.GetCapabilities() method that considers server mode - Update daemon_guard, daemon_autostart, daemons, main to use GetCapabilities() - Add TestGetCapabilities test - Update init command help text to document server mode flags The existing CapabilitiesForBackend(string) is kept for backward compatibility but now includes a note to use Config.GetCapabilities() when the full config is available. Co-Authored-By: Claude Opus 4.5 --- cmd/bd/daemon_autostart.go | 3 +- cmd/bd/daemon_guard.go | 6 ++-- cmd/bd/daemons.go | 5 ++-- cmd/bd/init.go | 7 ++++- cmd/bd/main.go | 5 ++-- internal/configfile/configfile.go | 18 ++++++++++++ internal/configfile/configfile_test.go | 39 ++++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 9 deletions(-) diff --git a/cmd/bd/daemon_autostart.go b/cmd/bd/daemon_autostart.go index 38ed8ff0..6168439c 100644 --- a/cmd/bd/daemon_autostart.go +++ b/cmd/bd/daemon_autostart.go @@ -69,7 +69,8 @@ func singleProcessOnlyBackend() bool { if err != nil || cfg == nil { return false } - return configfile.CapabilitiesForBackend(cfg.GetBackend()).SingleProcessOnly + // Use GetCapabilities() to properly handle Dolt server mode + return cfg.GetCapabilities().SingleProcessOnly } // shouldAutoStartDaemon checks if daemon auto-start is enabled diff --git a/cmd/bd/daemon_guard.go b/cmd/bd/daemon_guard.go index 717f5891..9a4eb73a 100644 --- a/cmd/bd/daemon_guard.go +++ b/cmd/bd/daemon_guard.go @@ -69,9 +69,9 @@ func guardDaemonStartForDolt(cmd *cobra.Command, _ []string) error { return nil } - backend := cfg.GetBackend() - if configfile.CapabilitiesForBackend(backend).SingleProcessOnly { - return fmt.Errorf("%s", singleProcessBackendHelp(backend)) + // Use GetCapabilities() to properly handle Dolt server mode + if cfg.GetCapabilities().SingleProcessOnly { + return fmt.Errorf("%s", singleProcessBackendHelp(cfg.GetBackend())) } return nil diff --git a/cmd/bd/daemons.go b/cmd/bd/daemons.go index 14ea6e79..03425275 100644 --- a/cmd/bd/daemons.go +++ b/cmd/bd/daemons.go @@ -255,11 +255,12 @@ Stops the daemon gracefully, then starts a new one.`, } workspace := targetDaemon.WorkspacePath - // Guardrail: don't (re)start daemons for single-process backends (e.g., Dolt). + // Guardrail: don't (re)start daemons for single-process backends (e.g., embedded Dolt). // This command may be run from a different workspace, so check the target workspace. + // Note: Dolt server mode supports multi-process, so GetCapabilities() is used. targetBeadsDir := beads.FollowRedirect(filepath.Join(workspace, ".beads")) if cfg, err := configfile.Load(targetBeadsDir); err == nil && cfg != nil { - if configfile.CapabilitiesForBackend(cfg.GetBackend()).SingleProcessOnly { + if cfg.GetCapabilities().SingleProcessOnly { if jsonOutput { outputJSON(map[string]string{"error": fmt.Sprintf("daemon mode is not supported for backend %q (single-process only)", cfg.GetBackend())}) } else { diff --git a/cmd/bd/init.go b/cmd/bd/init.go index 46d621b2..2d63e20c 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -40,7 +40,12 @@ to prevent deleted issues from being resurrected during re-initialization. With --stealth: configures per-repository git settings for invisible beads usage: • .git/info/exclude to prevent beads files from being committed • Claude Code settings with bd onboard instruction - Perfect for personal use without affecting repo collaborators.`, + Perfect for personal use without affecting repo collaborators. + +With --backend dolt --server: configures Dolt to connect to an external dolt sql-server +instead of using the embedded driver. This enables multi-writer access for multi-agent +environments. Connection settings can be customized with --server-host, --server-port, +and --server-user. Password should be set via BEADS_DOLT_PASSWORD environment variable.`, Run: func(cmd *cobra.Command, _ []string) { prefix, _ := cmd.Flags().GetString("prefix") quiet, _ := cmd.Flags().GetBool("quiet") diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 11795c18..ad779fdf 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -623,13 +623,14 @@ var rootCmd = &cobra.Command{ debug.Logf("wisp operation detected, using direct mode") } - // Dolt backend (embedded) is single-process-only; never use daemon/RPC. + // Embedded Dolt is single-process-only; never use daemon/RPC. + // (Dolt server mode supports multi-process and won't trigger this.) // This must be checked after dbPath is resolved. if !noDaemon && singleProcessOnlyBackend() { noDaemon = true daemonStatus.AutoStartEnabled = false daemonStatus.FallbackReason = FallbackSingleProcessOnly - daemonStatus.Detail = "backend is single-process-only (dolt): daemon mode disabled; using direct mode" + daemonStatus.Detail = "backend is single-process-only (embedded dolt): daemon mode disabled; using direct mode" debug.Logf("single-process backend detected, using direct mode") } diff --git a/internal/configfile/configfile.go b/internal/configfile/configfile.go index 1d8a8d53..94dc0501 100644 --- a/internal/configfile/configfile.go +++ b/internal/configfile/configfile.go @@ -176,17 +176,35 @@ type BackendCapabilities struct { // CapabilitiesForBackend returns capabilities for a backend string. // Unknown backends are treated conservatively as single-process-only. +// +// Note: For Dolt, this returns SingleProcessOnly=true for embedded mode. +// Use Config.GetCapabilities() when you have the full config to properly +// handle server mode (which supports multi-process access). func CapabilitiesForBackend(backend string) BackendCapabilities { switch strings.TrimSpace(strings.ToLower(backend)) { case "", BackendSQLite: return BackendCapabilities{SingleProcessOnly: false} case BackendDolt: + // Embedded Dolt is single-process-only. + // Server mode is handled by Config.GetCapabilities(). return BackendCapabilities{SingleProcessOnly: true} default: return BackendCapabilities{SingleProcessOnly: true} } } +// GetCapabilities returns the backend capabilities for this config. +// Unlike CapabilitiesForBackend(string), this considers Dolt server mode +// which supports multi-process access. +func (c *Config) GetCapabilities() BackendCapabilities { + backend := c.GetBackend() + if backend == BackendDolt && c.IsDoltServerMode() { + // Server mode supports multi-writer, so NOT single-process-only + return BackendCapabilities{SingleProcessOnly: false} + } + return CapabilitiesForBackend(backend) +} + // GetBackend returns the configured backend type, defaulting to SQLite. func (c *Config) GetBackend() string { if c.Backend == "" { diff --git a/internal/configfile/configfile_test.go b/internal/configfile/configfile_test.go index 0cfe1886..be97dbbe 100644 --- a/internal/configfile/configfile_test.go +++ b/internal/configfile/configfile_test.go @@ -338,6 +338,45 @@ func TestDoltServerMode(t *testing.T) { }) } +// TestGetCapabilities tests that GetCapabilities properly handles server mode +func TestGetCapabilities(t *testing.T) { + tests := []struct { + name string + cfg *Config + wantSingleProc bool + }{ + { + name: "sqlite is multi-process", + cfg: &Config{Backend: BackendSQLite}, + wantSingleProc: false, + }, + { + name: "dolt embedded is single-process", + cfg: &Config{Backend: BackendDolt, DoltMode: DoltModeEmbedded}, + wantSingleProc: true, + }, + { + name: "dolt default (empty) is single-process", + cfg: &Config{Backend: BackendDolt}, + wantSingleProc: true, + }, + { + name: "dolt server mode is multi-process", + cfg: &Config{Backend: BackendDolt, DoltMode: DoltModeServer}, + wantSingleProc: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cfg.GetCapabilities().SingleProcessOnly + if got != tt.wantSingleProc { + t.Errorf("GetCapabilities().SingleProcessOnly = %v, want %v", got, tt.wantSingleProc) + } + }) + } +} + // TestDoltServerModeRoundtrip tests that server mode config survives save/load func TestDoltServerModeRoundtrip(t *testing.T) { tmpDir := t.TempDir()