fix(handoff): preserve tmux session by setting remain-on-exit before kill
When gt handoff killed pane processes before respawning, the pane would be destroyed (since remain-on-exit defaults to off), causing respawn-pane to fail with "can't find pane" error. Fix: Set remain-on-exit=on before killing processes, so the pane survives process death and can be respawned. This restores tmux session reuse on handoffs. Changes: - Add SetRemainOnExit method to tmux package - Call SetRemainOnExit(true) before KillPaneProcesses in: - Local handoff (runHandoff) - Remote handoff (handoffRemoteSession) - Mayor attach respawn (runMayorAttach) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -204,6 +204,13 @@ func runHandoff(cmd *cobra.Command, args []string) error {
|
|||||||
_ = os.WriteFile(markerPath, []byte(currentSession), 0644)
|
_ = 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)
|
||||||
|
}
|
||||||
|
|
||||||
// Kill all processes in the pane before respawning to prevent orphan leaks
|
// Kill all processes in the pane before respawning to prevent orphan leaks
|
||||||
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
|
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
|
||||||
if err := t.KillPaneProcesses(pane); err != nil {
|
if err := t.KillPaneProcesses(pane); err != nil {
|
||||||
@@ -212,6 +219,7 @@ func runHandoff(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use exec to respawn the pane - this kills us and restarts
|
// Use exec to respawn the pane - this kills us and restarts
|
||||||
|
// Note: respawn-pane automatically resets remain-on-exit to off
|
||||||
return t.RespawnPane(pane, restartCmd)
|
return t.RespawnPane(pane, restartCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +575,13 @@ func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error
|
|||||||
return nil
|
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
|
// Kill all processes in the pane before respawning to prevent orphan leaks
|
||||||
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
|
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
|
||||||
if err := t.KillPaneProcesses(targetPane); err != nil {
|
if err := t.KillPaneProcesses(targetPane); err != nil {
|
||||||
@@ -581,6 +596,7 @@ func handoffRemoteSession(t *tmux.Tmux, targetSession, restartCmd string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Respawn the remote session's pane
|
// Respawn the remote session's pane
|
||||||
|
// Note: respawn-pane automatically resets remain-on-exit to off
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,12 @@ func runMayorAttach(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("building startup command: %w", err)
|
return fmt.Errorf("building startup command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set remain-on-exit so the pane survives process death during respawn.
|
||||||
|
// Without this, killing processes causes tmux to destroy the pane.
|
||||||
|
if err := t.SetRemainOnExit(paneID, 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
|
// Kill all processes in the pane before respawning to prevent orphan leaks
|
||||||
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
|
// RespawnPane's -k flag only sends SIGHUP which Claude/Node may ignore
|
||||||
if err := t.KillPaneProcesses(paneID); err != nil {
|
if err := t.KillPaneProcesses(paneID); err != nil {
|
||||||
@@ -207,6 +213,7 @@ func runMayorAttach(cmd *cobra.Command, args []string) error {
|
|||||||
style.PrintWarning("could not kill pane processes: %v", err)
|
style.PrintWarning("could not kill pane processes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: respawn-pane automatically resets remain-on-exit to off
|
||||||
if err := t.RespawnPane(paneID, startupCmd); err != nil {
|
if err := t.RespawnPane(paneID, startupCmd); err != nil {
|
||||||
return fmt.Errorf("restarting runtime: %w", err)
|
return fmt.Errorf("restarting runtime: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1385,6 +1385,19 @@ func (t *Tmux) ClearHistory(pane string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRemainOnExit controls whether a pane stays around after its process exits.
|
||||||
|
// When on, the pane remains with "[Exited]" status, allowing respawn-pane to restart it.
|
||||||
|
// When off (default), the pane is destroyed when its process exits.
|
||||||
|
// This is essential for handoff: set on before killing processes, so respawn-pane works.
|
||||||
|
func (t *Tmux) SetRemainOnExit(pane string, on bool) error {
|
||||||
|
value := "on"
|
||||||
|
if !on {
|
||||||
|
value = "off"
|
||||||
|
}
|
||||||
|
_, err := t.run("set-option", "-t", pane, "remain-on-exit", value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// SwitchClient switches the current tmux client to a different session.
|
// SwitchClient switches the current tmux client to a different session.
|
||||||
// Used after remote recycle to move the user's view to the recycled session.
|
// Used after remote recycle to move the user's view to the recycled session.
|
||||||
func (t *Tmux) SwitchClient(targetSession string) error {
|
func (t *Tmux) SwitchClient(targetSession string) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user