fix(tmux): use NewSessionWithCommand to avoid send-keys race condition
Agent sessions would fail on startup because send-keys arrived before the shell was ready, causing 'bad pattern' and 'command not found' errors. Fix: Create sessions with the command directly using tmux new-session's command argument. This runs the agent as the pane's initial process, avoiding shell readiness timing issues entirely. Updated all agent managers: mayor, deacon, witness, refinery, polecat, crew. Also fixes pre-existing build error in polecat/manager.go (polecatPath → clonePath/newClonePath). Closes #280 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -281,7 +281,7 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
||||
|
||||
// Copy overlay files from .runtime/overlay/ to polecat root.
|
||||
// This allows services to have .env and other config files at their root.
|
||||
if err := rig.CopyOverlay(m.rig.Path, polecatPath); err != nil {
|
||||
if err := rig.CopyOverlay(m.rig.Path, clonePath); err != nil {
|
||||
// Non-fatal - log warning but continue
|
||||
fmt.Printf("Warning: could not copy overlay files: %v\n", err)
|
||||
}
|
||||
@@ -538,7 +538,7 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
||||
}
|
||||
|
||||
// Copy overlay files from .runtime/overlay/ to polecat root.
|
||||
if err := rig.CopyOverlay(m.rig.Path, polecatPath); err != nil {
|
||||
if err := rig.CopyOverlay(m.rig.Path, newClonePath); err != nil {
|
||||
fmt.Printf("Warning: could not copy overlay files: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -787,9 +787,9 @@ func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
|
||||
|
||||
// setupSharedBeads creates a redirect file so the polecat uses the rig's shared .beads database.
|
||||
// This eliminates the need for git sync between polecat clones - all polecats share one database.
|
||||
func (m *Manager) setupSharedBeads(polecatPath string) error {
|
||||
func (m *Manager) setupSharedBeads(clonePath string) error {
|
||||
townRoot := filepath.Dir(m.rig.Path)
|
||||
return beads.SetupRedirect(townRoot, polecatPath)
|
||||
return beads.SetupRedirect(townRoot, clonePath)
|
||||
}
|
||||
|
||||
// CleanupStaleBranches removes orphaned polecat branches that are no longer in use.
|
||||
|
||||
@@ -168,8 +168,19 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
|
||||
return fmt.Errorf("ensuring runtime settings: %w", err)
|
||||
}
|
||||
|
||||
// Create session
|
||||
if err := m.tmux.NewSession(sessionID, workDir); err != nil {
|
||||
// Build startup command first
|
||||
command := opts.Command
|
||||
if command == "" {
|
||||
command = config.BuildPolecatStartupCommand(m.rig.Name, polecat, m.rig.Path, "")
|
||||
}
|
||||
// Prepend runtime config dir env if needed
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && opts.RuntimeConfigDir != "" {
|
||||
command = config.PrependEnv(command, map[string]string{runtimeConfig.Session.ConfigDirEnv: opts.RuntimeConfigDir})
|
||||
}
|
||||
|
||||
// Create session with command directly to avoid send-keys race condition.
|
||||
// See: https://github.com/anthropics/gastown/issues/280
|
||||
if err := m.tmux.NewSessionWithCommand(sessionID, workDir, command); err != nil {
|
||||
return fmt.Errorf("creating session: %w", err)
|
||||
}
|
||||
|
||||
@@ -205,24 +216,6 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
|
||||
agentID := fmt.Sprintf("%s/%s", m.rig.Name, polecat)
|
||||
debugSession("SetPaneDiedHook", m.tmux.SetPaneDiedHook(sessionID, agentID))
|
||||
|
||||
// Send initial command with env vars exported inline
|
||||
command := opts.Command
|
||||
if command == "" {
|
||||
command = config.BuildPolecatStartupCommand(m.rig.Name, polecat, m.rig.Path, "")
|
||||
}
|
||||
// Prepend runtime config dir env if needed
|
||||
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && opts.RuntimeConfigDir != "" {
|
||||
command = config.PrependEnv(command, map[string]string{runtimeConfig.Session.ConfigDirEnv: opts.RuntimeConfigDir})
|
||||
}
|
||||
// Wait for shell to be ready before sending keys (prevents "can't find pane" under load)
|
||||
if err := m.tmux.WaitForShellReady(sessionID, 5*time.Second); err != nil {
|
||||
_ = m.tmux.KillSession(sessionID)
|
||||
return fmt.Errorf("waiting for shell: %w", err)
|
||||
}
|
||||
if err := m.tmux.SendKeys(sessionID, command); err != nil {
|
||||
return fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
debugSession("WaitForCommand", m.tmux.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user