fix(tmux): kill pane processes before all RespawnPane calls to prevent orphans

## Problem
Claude processes were accumulating as orphans, with 100+ processes piling up
daily. Every `gt handoff` (used dozens of times/hour by crew) left orphaned
processes because `tmux respawn-pane -k` only sends SIGHUP, which Node/Claude
ignores.

## Root Cause
Previous fixes (1043f00d, f89ac47f, 2feefd17, 1b036aad) were laser-focused on
specific symptoms (shutdown, setsid, done.go, molecule_step.go) but never did
a comprehensive audit of ALL RespawnPane call sites. handoff.go was never
fixed despite being the main source of orphans.

## Solution
Added KillPaneProcesses() call before every RespawnPane() in:
- handoff.go (self handoff and remote handoff)
- mayor.go (mayor restart)
- crew_at.go (new session and restart)

KillPaneProcesses explicitly kills all descendant processes with SIGTERM/SIGKILL
before respawning, preventing orphans regardless of SIGHUP handling.

molecule_step.go already had this fix from commit 1b036aad.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
mayor
2026-01-21 21:09:15 -08:00
committed by beads/crew/emma
parent 02390251fc
commit 94c2d71c13
3 changed files with 33 additions and 0 deletions

View File

@@ -209,6 +209,12 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && claudeConfigDir != "" {
startupCmd = config.PrependEnv(startupCmd, map[string]string{runtimeConfig.Session.ConfigDirEnv: claudeConfigDir})
}
// Kill all processes in the pane before respawning to prevent orphan leaks
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
if err := t.KillPaneProcesses(paneID); err != nil {
// Non-fatal but log the warning
style.PrintWarning("could not kill pane processes: %v", err)
}
if err := t.RespawnPane(paneID, startupCmd); err != nil {
return fmt.Errorf("starting runtime: %w", err)
}
@@ -252,6 +258,12 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && claudeConfigDir != "" {
startupCmd = config.PrependEnv(startupCmd, map[string]string{runtimeConfig.Session.ConfigDirEnv: claudeConfigDir})
}
// Kill all processes in the pane before respawning to prevent orphan leaks
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
if err := t.KillPaneProcesses(paneID); err != nil {
// Non-fatal but log the warning
style.PrintWarning("could not kill pane processes: %v", err)
}
if err := t.RespawnPane(paneID, startupCmd); err != nil {
return fmt.Errorf("restarting runtime: %w", err)
}