fix(tmux): use KillSessionWithProcesses to prevent zombie bash processes

When Claude sessions were terminated using KillSession(), bash subprocesses
spawned by Claude's Bash tool could survive because they ignore SIGHUP.
This caused zombie processes to accumulate over time.

Changed all critical session termination paths to use KillSessionWithProcesses()
which explicitly kills all descendant processes before terminating the session.

Fixes: gt-ew3tk

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
slit
2026-01-20 20:37:34 -08:00
committed by beads/crew/emma
parent 78ca8bd5bf
commit 9caf5302d4
18 changed files with 66 additions and 39 deletions

View File

@@ -301,9 +301,10 @@ func runDegradedTriage(b *boot.Boot) (action, target string, err error) {
// Nudge the session to try to wake it up
age := hb.Age()
if age > 30*time.Minute {
// Very stuck - restart the session
// Very stuck - restart the session.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
fmt.Printf("Deacon heartbeat is %s old - restarting session\n", age.Round(time.Minute))
if err := tm.KillSession(deaconSession); err == nil {
if err := tm.KillSessionWithProcesses(deaconSession); err == nil {
return "restart", "deacon-stuck", nil
}
} else {

View File

@@ -28,11 +28,12 @@ func runCrewRename(cmd *cobra.Command, args []string) error {
return err
}
// Kill any running session for the old name
// Kill any running session for the old name.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
t := tmux.NewTmux()
oldSessionID := crewSessionName(r.Name, oldName)
if hasSession, _ := t.HasSession(oldSessionID); hasSession {
if err := t.KillSession(oldSessionID); err != nil {
if err := t.KillSessionWithProcesses(oldSessionID); err != nil {
return fmt.Errorf("killing old session: %w", err)
}
fmt.Printf("Killed session %s\n", oldSessionID)

View File

@@ -491,8 +491,9 @@ func runDeaconStop(cmd *cobra.Command, args []string) error {
_ = t.SendKeysRaw(sessionName, "C-c")
time.Sleep(100 * time.Millisecond)
// Kill the session
if err := t.KillSession(sessionName); err != nil {
// Kill the session.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
if err := t.KillSessionWithProcesses(sessionName); err != nil {
return fmt.Errorf("killing session: %w", err)
}
@@ -592,8 +593,9 @@ func runDeaconRestart(cmd *cobra.Command, args []string) error {
fmt.Println("Restarting Deacon...")
if running {
// Kill existing session
if err := t.KillSession(sessionName); err != nil {
// Kill existing session.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
if err := t.KillSessionWithProcesses(sessionName); err != nil {
style.PrintWarning("failed to kill session: %v", err)
}
}
@@ -876,9 +878,10 @@ func runDeaconForceKill(cmd *cobra.Command, args []string) error {
mailBody := fmt.Sprintf("Deacon detected %s as unresponsive.\nReason: %s\nAction: force-killing session", agent, reason)
sendMail(townRoot, agent, "FORCE_KILL: unresponsive", mailBody)
// Step 2: Kill the tmux session
// Step 2: Kill the tmux session.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
fmt.Printf("%s Killing tmux session %s...\n", style.Dim.Render("2."), sessionName)
if err := t.KillSession(sessionName); err != nil {
if err := t.KillSessionWithProcesses(sessionName); err != nil {
return fmt.Errorf("killing session: %w", err)
}

View File

@@ -192,12 +192,13 @@ func runWitnessStop(cmd *cobra.Command, args []string) error {
return err
}
// Kill tmux session if it exists
// Kill tmux session if it exists.
// Use KillSessionWithProcesses to ensure all descendant processes are killed.
t := tmux.NewTmux()
sessionName := witnessSessionName(rigName)
running, _ := t.HasSession(sessionName)
if running {
if err := t.KillSession(sessionName); err != nil {
if err := t.KillSessionWithProcesses(sessionName); err != nil {
style.PrintWarning("failed to kill session: %v", err)
}
}