fix(version): add file-based caching to prevent bd version contention

Under high concurrency (17+ agents), the bd version check spawns
multiple git subprocesses per invocation, causing timeouts when
85-120+ git processes compete for resources.

This fix:
- Caches successful version checks to ~/.cache/gastown/beads-version.json
- Uses cached results for 24 hours to avoid subprocess spawning
- On timeout, uses stale cache if available or gracefully degrades
- Prints warning when using cached/degraded path

Fixes: https://github.com/steveyegge/gastown/issues/503

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
furiosa
2026-01-24 09:39:07 -08:00
committed by John Ogle
parent 0d665e6cd7
commit 551da0b159

View File

@@ -3,13 +3,18 @@ package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/steveyegge/gastown/internal/state"
)
// MinBeadsVersion is the minimum required beads version for Gas Town.
@@ -90,6 +95,58 @@ func (v beadsVersion) compare(other beadsVersion) int {
// Pre-compiled regex for beads version parsing
var beadsVersionRe = regexp.MustCompile(`bd version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)`)
// versionCacheTTL is how long a cached version check remains valid.
// 24 hours is reasonable since version upgrades are infrequent.
const versionCacheTTL = 24 * time.Hour
// versionCache stores the result of a beads version check.
type versionCache struct {
Version string `json:"version"`
CheckedAt time.Time `json:"checked_at"`
Valid bool `json:"valid"` // true if version meets minimum requirement
}
// versionCachePath returns the path to the version cache file.
func versionCachePath() string {
return filepath.Join(state.CacheDir(), "beads-version.json")
}
// loadVersionCache reads the cached version check result.
func loadVersionCache() (*versionCache, error) {
data, err := os.ReadFile(versionCachePath())
if err != nil {
return nil, err
}
var cache versionCache
if err := json.Unmarshal(data, &cache); err != nil {
return nil, err
}
return &cache, nil
}
// saveVersionCache writes the version check result to cache.
func saveVersionCache(c *versionCache) error {
dir := state.CacheDir()
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
// Atomic write via temp file
tmp := versionCachePath() + ".tmp"
if err := os.WriteFile(tmp, data, 0600); err != nil {
return err
}
return os.Rename(tmp, versionCachePath())
}
// isCacheFresh returns true if the cache is within the TTL.
func (c *versionCache) isCacheFresh() bool {
return time.Since(c.CheckedAt) < versionCacheTTL
}
func getBeadsVersion() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -132,8 +189,27 @@ func CheckBeadsVersion() error {
}
func checkBeadsVersionInternal() error {
// Try to use cached result first to avoid subprocess spawning
if cache, err := loadVersionCache(); err == nil && cache.isCacheFresh() {
if cache.Valid {
return nil // Cached successful check
}
// Cached failure - still need to check (version might have been upgraded)
}
installedStr, err := getBeadsVersion()
if err != nil {
// On timeout, try to use stale cache or gracefully degrade
if strings.Contains(err.Error(), "timed out") {
if cache, cacheErr := loadVersionCache(); cacheErr == nil && cache.Valid {
// Use stale cache but warn
fmt.Fprintf(os.Stderr, "Warning: bd version check timed out, using cached result (v%s)\n", cache.Version)
return nil
}
// No cache available - gracefully degrade with warning
fmt.Fprintf(os.Stderr, "Warning: bd version check timed out (high system load?), proceeding anyway\n")
return nil
}
return fmt.Errorf("cannot verify beads version: %w", err)
}
@@ -148,7 +224,16 @@ func checkBeadsVersionInternal() error {
return fmt.Errorf("cannot parse required beads version %q: %w", MinBeadsVersion, err)
}
if installed.compare(required) < 0 {
valid := installed.compare(required) >= 0
// Cache the result
_ = saveVersionCache(&versionCache{
Version: installedStr,
CheckedAt: time.Now(),
Valid: valid,
})
if !valid {
return fmt.Errorf("beads version %s is required, but %s is installed\n\nPlease upgrade beads: go install github.com/steveyegge/beads/cmd/bd@latest", MinBeadsVersion, installedStr)
}