diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 3d8cd839..f6b29a80 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -204,25 +204,17 @@ func runHandoff(cmd *cobra.Command, args []string) error { _ = os.WriteFile(markerPath, []byte(currentSession), 0644) } - // Set remain-on-exit so the pane survives process death during handoff. - // Without this, killing processes causes tmux to destroy the pane before - // we can respawn it. This is essential for tmux session reuse. - if err := t.SetRemainOnExit(pane, true); err != nil { - style.PrintWarning("could not set remain-on-exit: %v", err) - } + // NOTE: We intentionally do NOT kill pane processes before respawning (hq-bv7ef). + // Previous approach (KillPaneProcessesExcluding) killed the pane's main process, + // which caused the pane to close (remain-on-exit is off by default), making + // RespawnPane fail because the target pane no longer exists. + // + // The respawn-pane -k flag handles killing atomically - it kills the old process + // and starts the new one in a single operation without closing the pane. + // If orphan processes remain (e.g., Claude ignoring SIGHUP), they will be cleaned + // up when the new session starts or when the Witness runs periodic cleanup. - // Kill all processes in the pane before respawning to prevent orphan leaks - // RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore - // IMPORTANT: Exclude our own process to avoid race condition where we get killed - // before RespawnPane executes, causing the pane to close prematurely (gt-85qd) - myPID := fmt.Sprintf("%d", os.Getpid()) - if err := t.KillPaneProcessesExcluding(pane, []string{myPID}); err != nil { - // Non-fatal but log the warning - style.PrintWarning("could not kill pane processes: %v", err) - } - - // Use exec to respawn the pane - this kills us and restarts - // Note: respawn-pane automatically resets remain-on-exit to off + // Use respawn-pane to atomically kill old process and start new one return t.RespawnPane(pane, restartCmd) } @@ -578,19 +570,10 @@ func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error return nil } - // Set remain-on-exit so the pane survives process death during handoff. - // Without this, killing processes causes tmux to destroy the pane before - // we can respawn it. This is essential for tmux session reuse. - if err := t.SetRemainOnExit(targetPane, true); err != nil { - style.PrintWarning("could not set remain-on-exit: %v", err) - } - - // 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(targetPane); err != nil { - // Non-fatal but log the warning - style.PrintWarning("could not kill pane processes: %v", err) - } + // NOTE: We intentionally do NOT kill pane processes before respawning (hq-bv7ef). + // Previous approach (KillPaneProcesses) killed the pane's main process, which caused + // the pane to close (remain-on-exit is off by default), making RespawnPane fail. + // The respawn-pane -k flag handles killing atomically without closing the pane. // Clear scrollback history before respawn (resets copy-mode from [0/N] to [0/0]) if err := t.ClearHistory(targetPane); err != nil { @@ -598,8 +581,7 @@ func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error style.PrintWarning("could not clear history: %v", err) } - // Respawn the remote session's pane - // Note: respawn-pane automatically resets remain-on-exit to off + // Respawn the remote session's pane - -k flag atomically kills old process and starts new one if err := t.RespawnPane(targetPane, restartCmd); err != nil { return fmt.Errorf("respawning pane: %w", err) }