From 354219033a8ca67da13b53fd189c5a696efcdce1 Mon Sep 17 00:00:00 2001 From: keeper Date: Fri, 2 Jan 2026 18:51:41 -0800 Subject: [PATCH] feat: Add 'ensure' semantics to witness/refinery start commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gt witness start and gt refinery start now detect zombie sessions (tmux alive but Claude dead) and automatically kill and recreate them. This makes the start commands idempotent: - If no session exists → create new session - If session exists and healthy → do nothing (already running) - If session exists but zombie → kill and recreate Previously users had to manually run stop then start, or use restart. Closes: gt-ekc5u 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/witness.go | 14 ++++++++++++-- internal/refinery/manager.go | 11 ++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/internal/cmd/witness.go b/internal/cmd/witness.go index 6a4876ee..7b94ade1 100644 --- a/internal/cmd/witness.go +++ b/internal/cmd/witness.go @@ -284,7 +284,8 @@ func witnessSessionName(rigName string) string { } // ensureWitnessSession creates a witness tmux session if it doesn't exist. -// Returns true if a new session was created, false if it already existed. +// Returns true if a new session was created, false if it already existed (and is healthy). +// Implements 'ensure' semantics: if session exists but Claude is dead (zombie), kills and recreates. func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) { t := tmux.NewTmux() sessionName := witnessSessionName(rigName) @@ -296,7 +297,16 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) { } if running { - return false, nil + // Session exists - check if Claude is actually running (healthy vs zombie) + if t.IsClaudeRunning(sessionName) { + // Healthy - Claude is running + return false, nil + } + // Zombie - tmux alive but Claude dead. Kill and recreate. + fmt.Printf("%s Detected zombie session (tmux alive, Claude dead). Recreating...\n", style.Dim.Render("⚠")) + if err := t.KillSession(sessionName); err != nil { + return false, fmt.Errorf("killing zombie session: %w", err) + } } // Working directory is the witness's rig clone (if it exists) or witness dir diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 674f627a..dc1fb8ef 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -137,7 +137,16 @@ func (m *Manager) Start(foreground bool) error { // Background mode: check if session already exists running, _ := t.HasSession(sessionID) if running { - return ErrAlreadyRunning + // Session exists - check if Claude is actually running (healthy vs zombie) + if t.IsClaudeRunning(sessionID) { + // Healthy - Claude is running + return ErrAlreadyRunning + } + // Zombie - tmux alive but Claude dead. Kill and recreate. + fmt.Fprintln(m.output, "⚠ Detected zombie session (tmux alive, Claude dead). Recreating...") + if err := t.KillSession(sessionID); err != nil { + return fmt.Errorf("killing zombie session: %w", err) + } } // Also check via PID for backwards compatibility