fix(crew): parallelize crew start to prevent hanging

Start crew members concurrently instead of sequentially. Previously,
`gt crew start --all` could hang for minutes because each crew member
was started one at a time, with each waiting up to 60 seconds for
Claude to initialize.

With parallel startup, all crew members start simultaneously and
the total wait time is bounded by the slowest individual startup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-09 21:24:23 -08:00
committed by Steve Yegge
parent be35b3eaab
commit cb2b130ca2

View File

@@ -1,15 +1,18 @@
package cmd
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/mail"
@@ -300,28 +303,73 @@ func runCrewStart(cmd *cobra.Command, args []string) error {
}
}
// Start each crew member
// Resolve account config once for all crew members
townRoot, _ := workspace.Find(r.Path)
if townRoot == "" {
townRoot = filepath.Dir(r.Path)
}
accountsPath := constants.MayorAccountsPath(townRoot)
claudeConfigDir, _, _ := config.ResolveAccountConfigDir(accountsPath, crewAccount)
// Build start options (shared across all crew members)
opts := crew.StartOptions{
Account: crewAccount,
ClaudeConfigDir: claudeConfigDir,
AgentOverride: crewAgentOverride,
}
// Start each crew member in parallel
type result struct {
name string
err error
skipped bool // true if session was already running
}
results := make(chan result, len(crewNames))
var wg sync.WaitGroup
fmt.Printf("Starting %d crew member(s) in %s...\n", len(crewNames), rigName)
for _, name := range crewNames {
wg.Add(1)
go func(crewName string) {
defer wg.Done()
err := crewMgr.Start(crewName, opts)
skipped := errors.Is(err, crew.ErrSessionRunning)
if skipped {
err = nil // Not an error, just already running
}
results <- result{name: crewName, err: err, skipped: skipped}
}(name)
}
// Wait for all goroutines to complete
go func() {
wg.Wait()
close(results)
}()
// Collect results
var lastErr error
startedCount := 0
for _, name := range crewNames {
// Set the start.go flags before calling runStartCrew
startCrewRig = rigName
startCrewAccount = crewAccount
startCrewAgentOverride = crewAgentOverride
// Use rig/name format for runStartCrew
fullName := rigName + "/" + name
if err := runStartCrew(cmd, []string{fullName}); err != nil {
fmt.Printf("Error starting %s/%s: %v\n", rigName, name, err)
lastErr = err
skippedCount := 0
for res := range results {
if res.err != nil {
fmt.Printf(" %s %s/%s: %v\n", style.ErrorPrefix, rigName, res.name, res.err)
lastErr = res.err
} else if res.skipped {
fmt.Printf(" %s %s/%s: already running\n", style.Dim.Render("○"), rigName, res.name)
skippedCount++
} else {
fmt.Printf(" %s %s/%s: started\n", style.SuccessPrefix, rigName, res.name)
startedCount++
}
}
if startedCount > 0 {
fmt.Printf("\n%s Started %d crew member(s) in %s\n",
style.Bold.Render("✓"), startedCount, r.Name)
// Summary
fmt.Println()
if startedCount > 0 || skippedCount > 0 {
fmt.Printf("%s Started %d, skipped %d (already running) in %s\n",
style.Bold.Render("✓"), startedCount, skippedCount, r.Name)
}
return lastErr