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:
jack
2026-01-08 23:35:31 -08:00
committed by Steve Yegge
parent a91e6cd643
commit afff85cdff
8 changed files with 99 additions and 138 deletions

View File

@@ -151,13 +151,19 @@ func (m *Manager) Start(foreground bool) error {
return fmt.Errorf("ensuring Claude settings: %w", err)
}
// Create new tmux session
if err := t.NewSession(sessionID, witnessDir); err != nil {
// Build startup command first
// Pass m.rig.Path so rig agent settings are honored (not town-level defaults)
bdActor := fmt.Sprintf("%s/witness", m.rig.Name)
command := config.BuildAgentStartupCommand("witness", bdActor, m.rig.Path, "")
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
// Create session with command directly to avoid send-keys race condition.
// See: https://github.com/anthropics/gastown/issues/280
if err := t.NewSessionWithCommand(sessionID, witnessDir, command); err != nil {
return fmt.Errorf("creating tmux session: %w", err)
}
// Set environment variables (non-fatal: session works without these)
bdActor := fmt.Sprintf("%s/witness", m.rig.Name)
_ = t.SetEnvironment(sessionID, "GT_ROLE", "witness")
_ = t.SetEnvironment(sessionID, "GT_RIG", m.rig.Name)
_ = t.SetEnvironment(sessionID, "BD_ACTOR", bdActor)
@@ -177,23 +183,6 @@ func (m *Manager) Start(foreground bool) error {
return fmt.Errorf("saving state: %w", err)
}
// Launch Claude directly (no shell respawn loop)
// Restarts are handled by daemon via LIFECYCLE mail or deacon health-scan
// NOTE: No gt prime injection needed - SessionStart hook handles it automatically
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
// Pass m.rig.Path so rig agent settings are honored (not town-level defaults)
command := config.BuildAgentStartupCommand("witness", bdActor, m.rig.Path, "")
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
// Wait for shell to be ready before sending keys (prevents "can't find pane" under load)
if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil {
_ = t.KillSession(sessionID)
return fmt.Errorf("waiting for shell: %w", err)
}
if err := t.SendKeys(sessionID, command); err != nil {
_ = t.KillSession(sessionID) // best-effort cleanup
return fmt.Errorf("starting Claude agent: %w", err)
}
// Wait for runtime to start and show its prompt (non-fatal)
if err := t.WaitForRuntimeReady(sessionID, runtimeConfig, constants.ClaudeStartTimeout); err != nil {
// Non-fatal - try to continue anyway