From 62fb0243b50b735efa632d992133ce4ef3c55477 Mon Sep 17 00:00:00 2001 From: furiosa Date: Sat, 24 Jan 2026 15:56:41 -0800 Subject: [PATCH] fix(handoff): don't kill pane processes before respawn (hq-bv7ef) The previous approach using KillPaneProcessesExcluding/KillPaneProcesses killed the pane's main process (Claude/node) before calling RespawnPane. This caused the pane to close (since tmux's remain-on-exit is off by default), which then made 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 in between. If orphan processes remain (e.g., Claude ignoring SIGHUP), they will be cleaned up when the new session starts or by periodic cleanup processes. This fixes both self-handoff and remote handoff paths. Fixes: hq-bv7ef Co-Authored-By: Claude Opus 4.5 --- internal/cmd/handoff.go | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 785d92c0..f6b29a80 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -204,17 +204,17 @@ func runHandoff(cmd *cobra.Command, args []string) error { _ = os.WriteFile(markerPath, []byte(currentSession), 0644) } - // 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) - } + // 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. - // Use exec to respawn the pane - this kills us and restarts + // Use respawn-pane to atomically kill old process and start new one return t.RespawnPane(pane, restartCmd) } @@ -570,12 +570,10 @@ func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error return nil } - // 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 { @@ -583,7 +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 + // 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) }