fix(handoff): don't kill pane processes before respawn (hq-bv7ef)
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 19s
CI / Test (push) Failing after 1m41s
CI / Lint (push) Failing after 22s
CI / Integration Tests (push) Successful in 1m15s
CI / Coverage Report (push) Has been skipped
Windows CI / Windows Build and Unit Tests (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
furiosa
2026-01-24 15:56:41 -08:00
committed by John Ogle
parent 94aaaa591a
commit 62fb0243b5

View File

@@ -204,17 +204,17 @@ func runHandoff(cmd *cobra.Command, args []string) error {
_ = os.WriteFile(markerPath, []byte(currentSession), 0644) _ = os.WriteFile(markerPath, []byte(currentSession), 0644)
} }
// Kill all processes in the pane before respawning to prevent orphan leaks // NOTE: We intentionally do NOT kill pane processes before respawning (hq-bv7ef).
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore // Previous approach (KillPaneProcessesExcluding) killed the pane's main process,
// IMPORTANT: Exclude our own process to avoid race condition where we get killed // which caused the pane to close (remain-on-exit is off by default), making
// before RespawnPane executes, causing the pane to close prematurely (gt-85qd) // RespawnPane fail because the target pane no longer exists.
myPID := fmt.Sprintf("%d", os.Getpid()) //
if err := t.KillPaneProcessesExcluding(pane, []string{myPID}); err != nil { // The respawn-pane -k flag handles killing atomically - it kills the old process
// Non-fatal but log the warning // and starts the new one in a single operation without closing the pane.
style.PrintWarning("could not kill pane processes: %v", err) // 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) return t.RespawnPane(pane, restartCmd)
} }
@@ -570,12 +570,10 @@ func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error
return nil return nil
} }
// Kill all processes in the pane before respawning to prevent orphan leaks // NOTE: We intentionally do NOT kill pane processes before respawning (hq-bv7ef).
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore // Previous approach (KillPaneProcesses) killed the pane's main process, which caused
if err := t.KillPaneProcesses(targetPane); err != nil { // the pane to close (remain-on-exit is off by default), making RespawnPane fail.
// Non-fatal but log the warning // The respawn-pane -k flag handles killing atomically without closing the pane.
style.PrintWarning("could not kill pane processes: %v", err)
}
// Clear scrollback history before respawn (resets copy-mode from [0/N] to [0/0]) // Clear scrollback history before respawn (resets copy-mode from [0/N] to [0/0])
if err := t.ClearHistory(targetPane); err != nil { 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) 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 { if err := t.RespawnPane(targetPane, restartCmd); err != nil {
return fmt.Errorf("respawning pane: %w", err) return fmt.Errorf("respawning pane: %w", err)
} }