fix(orphan): prevent Claude Code session leaks on macOS
Three bugs were causing orphaned Claude processes to accumulate: 1. TTY comparison in orphan.go checked for "?" but macOS shows "??" - Orphan cleanup never found anything on macOS - Changed to check for both "?" and "??" 2. selfKillSession in done.go used basic tmux kill-session - Claude Code can survive SIGHUP - Now uses KillSessionWithProcesses for proper cleanup 3. Crew stop commands used basic KillSession - Same issue as #2 - Updated runCrewRemove, runCrewStop, runCrewStopAll Root cause of 383 accumulated sessions: every gt done and crew stop left orphans, and the cleanup never worked on macOS. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,11 +60,11 @@ func runCrewRemove(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill session if it exists
|
// Kill session if it exists (with proper process cleanup to avoid orphans)
|
||||||
t := tmux.NewTmux()
|
t := tmux.NewTmux()
|
||||||
sessionID := crewSessionName(r.Name, name)
|
sessionID := crewSessionName(r.Name, name)
|
||||||
if hasSession, _ := t.HasSession(sessionID); hasSession {
|
if hasSession, _ := t.HasSession(sessionID); hasSession {
|
||||||
if err := t.KillSession(sessionID); err != nil {
|
if err := t.KillSessionWithProcesses(sessionID); err != nil {
|
||||||
fmt.Printf("Error killing session for %s: %v\n", arg, err)
|
fmt.Printf("Error killing session for %s: %v\n", arg, err)
|
||||||
lastErr = err
|
lastErr = err
|
||||||
continue
|
continue
|
||||||
@@ -591,8 +591,8 @@ func runCrewStop(cmd *cobra.Command, args []string) error {
|
|||||||
output, _ = t.CapturePane(sessionID, 50)
|
output, _ = t.CapturePane(sessionID, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill the session
|
// Kill the session (with proper process cleanup to avoid orphans)
|
||||||
if err := t.KillSession(sessionID); err != nil {
|
if err := t.KillSessionWithProcesses(sessionID); err != nil {
|
||||||
fmt.Printf(" %s [%s] %s: %s\n",
|
fmt.Printf(" %s [%s] %s: %s\n",
|
||||||
style.ErrorPrefix,
|
style.ErrorPrefix,
|
||||||
r.Name, name,
|
r.Name, name,
|
||||||
@@ -681,8 +681,8 @@ func runCrewStopAll() error {
|
|||||||
output, _ = t.CapturePane(sessionID, 50)
|
output, _ = t.CapturePane(sessionID, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill the session
|
// Kill the session (with proper process cleanup to avoid orphans)
|
||||||
if err := t.KillSession(sessionID); err != nil {
|
if err := t.KillSessionWithProcesses(sessionID); err != nil {
|
||||||
failed++
|
failed++
|
||||||
failures = append(failures, fmt.Sprintf("%s: %v", agentName, err))
|
failures = append(failures, fmt.Sprintf("%s: %v", agentName, err))
|
||||||
fmt.Printf(" %s %s\n", style.ErrorPrefix, agentName)
|
fmt.Printf(" %s %s\n", style.ErrorPrefix, agentName)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,6 +14,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/polecat"
|
"github.com/steveyegge/gastown/internal/polecat"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
"github.com/steveyegge/gastown/internal/townlog"
|
"github.com/steveyegge/gastown/internal/townlog"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
@@ -743,11 +743,11 @@ func selfKillSession(townRoot string, roleInfo RoleInfo) error {
|
|||||||
_ = events.LogFeed(events.TypeSessionDeath, agentID,
|
_ = events.LogFeed(events.TypeSessionDeath, agentID,
|
||||||
events.SessionDeathPayload(sessionName, agentID, "self-clean: done means gone", "gt done"))
|
events.SessionDeathPayload(sessionName, agentID, "self-clean: done means gone", "gt done"))
|
||||||
|
|
||||||
// Kill our own tmux session
|
// Kill our own tmux session with proper process cleanup
|
||||||
// This will terminate Claude and the shell, completing the self-cleaning cycle.
|
// This will terminate Claude and all child processes, completing the self-cleaning cycle.
|
||||||
// We use exec.Command instead of the tmux package to avoid import cycles.
|
// We use KillSessionWithProcesses to ensure no orphaned processes are left behind.
|
||||||
cmd := exec.Command("tmux", "kill-session", "-t", sessionName) //nolint:gosec // G204: sessionName is derived from env vars, not user input
|
t := tmux.NewTmux()
|
||||||
if err := cmd.Run(); err != nil {
|
if err := t.KillSessionWithProcesses(sessionName); err != nil {
|
||||||
return fmt.Errorf("killing session %s: %w", sessionName, err)
|
return fmt.Errorf("killing session %s: %w", sessionName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ func FindOrphanedClaudeProcesses() ([]OrphanedProcess, error) {
|
|||||||
etimeStr := fields[3]
|
etimeStr := fields[3]
|
||||||
|
|
||||||
// Only look for claude/codex processes without a TTY
|
// Only look for claude/codex processes without a TTY
|
||||||
if tty != "?" {
|
// Linux shows "?" for no TTY, macOS shows "??"
|
||||||
|
if tty != "?" && tty != "??" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user