fix(daemon): Kill zombie tmux sessions before recreating

The daemon was failing to restart agents when zombie tmux sessions existed
(session alive but Claude dead). Added EnsureSessionFresh() helper to
tmux package that:
- Checks if session exists
- If exists but Claude not running (zombie), kills the session
- Creates fresh session

Updated all daemon session creation points to use EnsureSessionFresh:
- ensureDeaconRunning()
- ensureWitnessRunning()
- restartPolecatSession()
- restartSession() in lifecycle.go

Added tests for the new helper function. (gt-j1i0r)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dag
2026-01-02 18:52:06 -08:00
committed by Steve Yegge
parent 883997e044
commit 9ad826cd8c
4 changed files with 137 additions and 4 deletions

View File

@@ -209,3 +209,98 @@ func TestWrapError(t *testing.T) {
}
}
}
func TestEnsureSessionFresh_NoExistingSession(t *testing.T) {
if !hasTmux() {
t.Skip("tmux not installed")
}
tm := NewTmux()
sessionName := "gt-test-fresh-" + t.Name()
// Clean up any existing session
_ = tm.KillSession(sessionName)
// EnsureSessionFresh should create a new session
if err := tm.EnsureSessionFresh(sessionName, ""); err != nil {
t.Fatalf("EnsureSessionFresh: %v", err)
}
defer func() { _ = tm.KillSession(sessionName) }()
// Verify session exists
has, err := tm.HasSession(sessionName)
if err != nil {
t.Fatalf("HasSession: %v", err)
}
if !has {
t.Error("expected session to exist after EnsureSessionFresh")
}
}
func TestEnsureSessionFresh_ZombieSession(t *testing.T) {
if !hasTmux() {
t.Skip("tmux not installed")
}
tm := NewTmux()
sessionName := "gt-test-zombie-" + t.Name()
// Clean up any existing session
_ = tm.KillSession(sessionName)
// Create a zombie session (session exists but no Claude/node running)
// A normal tmux session with bash/zsh is a "zombie" for our purposes
if err := tm.NewSession(sessionName, ""); err != nil {
t.Fatalf("NewSession: %v", err)
}
defer func() { _ = tm.KillSession(sessionName) }()
// Verify it's a zombie (not running Claude/node)
if tm.IsClaudeRunning(sessionName) {
t.Skip("session unexpectedly has Claude running - can't test zombie case")
}
// EnsureSessionFresh should kill the zombie and create fresh session
// This should NOT error with "session already exists"
if err := tm.EnsureSessionFresh(sessionName, ""); err != nil {
t.Fatalf("EnsureSessionFresh on zombie: %v", err)
}
// Session should still exist
has, err := tm.HasSession(sessionName)
if err != nil {
t.Fatalf("HasSession: %v", err)
}
if !has {
t.Error("expected session to exist after EnsureSessionFresh on zombie")
}
}
func TestEnsureSessionFresh_IdempotentOnZombie(t *testing.T) {
if !hasTmux() {
t.Skip("tmux not installed")
}
tm := NewTmux()
sessionName := "gt-test-idem-" + t.Name()
// Clean up any existing session
_ = tm.KillSession(sessionName)
// Call EnsureSessionFresh multiple times - should work each time
for i := 0; i < 3; i++ {
if err := tm.EnsureSessionFresh(sessionName, ""); err != nil {
t.Fatalf("EnsureSessionFresh attempt %d: %v", i+1, err)
}
}
defer func() { _ = tm.KillSession(sessionName) }()
// Session should exist
has, err := tm.HasSession(sessionName)
if err != nil {
t.Fatalf("HasSession: %v", err)
}
if !has {
t.Error("expected session to exist after multiple EnsureSessionFresh calls")
}
}