From ee34003f0252896eab9320cad4263d93ccfa858b Mon Sep 17 00:00:00 2001 From: beads/crew/dave Date: Fri, 9 Jan 2026 23:02:54 -0800 Subject: [PATCH] fix: SQLite uses DELETE mode on WSL2 Windows filesystem (GH#920) Auto-detect WSL2 environment with Windows filesystem paths (/mnt/c/, etc.) and fall back to DELETE journal mode instead of WAL. SQLite WAL mode does not work reliably across the WSL2/Windows 9P filesystem boundary due to shared-memory limitations. Detection checks: 1. Path matches /mnt/[a-z]/ pattern (Windows drive mount) 2. /proc/version contains microsoft or wsl Fixes #920 Co-Authored-By: Claude Opus 4.5 Executed-By: beads/crew/dave Rig: beads Role: crew --- internal/storage/sqlite/store.go | 42 ++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/internal/storage/sqlite/store.go b/internal/storage/sqlite/store.go index adaa1f8c..407489cb 100644 --- a/internal/storage/sqlite/store.go +++ b/internal/storage/sqlite/store.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "runtime" "strings" "sync" @@ -20,6 +21,27 @@ import ( "github.com/tetratelabs/wazero" ) +// wslWindowsPathPattern matches WSL paths to Windows filesystems like /mnt/c/, /mnt/d/, etc. +var wslWindowsPathPattern = regexp.MustCompile(`^/mnt/[a-zA-Z]/`) + +// isWSL2WindowsPath returns true if running under WSL2 and the path is on a Windows filesystem. +// SQLite WAL mode doesn't work reliably across the WSL2/Windows boundary (GH#920). +func isWSL2WindowsPath(path string) bool { + // Check if path looks like a Windows filesystem mounted in WSL (/mnt/c/, /mnt/d/, etc.) + if !wslWindowsPathPattern.MatchString(path) { + return false + } + + // Check if we're running under WSL by examining /proc/version + // WSL2 contains "microsoft" or "WSL" in the version string + data, err := os.ReadFile("/proc/version") + if err != nil { + return false // Not Linux or can't read - not WSL + } + version := strings.ToLower(string(data)) + return strings.Contains(version, "microsoft") || strings.Contains(version, "wsl") +} + // SQLiteStorage implements the Storage interface using SQLite type SQLiteStorage struct { db *sql.DB @@ -139,9 +161,15 @@ func NewWithTimeout(ctx context.Context, path string, busyTimeout time.Duration) } // For file-based databases, enable WAL mode once after opening the connection. + // Exception: On WSL2 with Windows filesystem (/mnt/c/), WAL doesn't work reliably + // due to shared-memory limitations across the 9P filesystem boundary (GH#920). if !isInMemory { - if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { - return nil, fmt.Errorf("failed to enable WAL mode: %w", err) + journalMode := "WAL" + if isWSL2WindowsPath(path) { + journalMode = "DELETE" // Fallback for WSL2 Windows filesystem + } + if _, err := db.Exec("PRAGMA journal_mode=" + journalMode); err != nil { + return nil, fmt.Errorf("failed to enable %s mode: %w", journalMode, err) } } @@ -476,13 +504,17 @@ func (s *SQLiteStorage) reconnect() error { // Restore connection pool settings s.configureConnectionPool(db) - // Re-enable WAL mode for file-based databases + // Re-enable WAL mode for file-based databases (or DELETE for WSL2 Windows paths) isInMemory := s.dbPath == ":memory:" || (strings.HasPrefix(s.connStr, "file:") && strings.Contains(s.connStr, "mode=memory")) if !isInMemory { - if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + journalMode := "WAL" + if isWSL2WindowsPath(s.dbPath) { + journalMode = "DELETE" // Fallback for WSL2 Windows filesystem (GH#920) + } + if _, err := db.Exec("PRAGMA journal_mode=" + journalMode); err != nil { _ = db.Close() - return fmt.Errorf("failed to enable WAL mode on reconnect: %w", err) + return fmt.Errorf("failed to enable %s mode on reconnect: %w", journalMode, err) } }