Add graceful shutdown mode to gt shutdown
gt shutdown --graceful sends ESC to all agents, requests handoff, waits for them to save state (default 30s), then terminates. Phases: 1. Send ESC to interrupt agents 2. Send shutdown message requesting handoff 3. Wait for agents to complete (configurable with --wait) 4. Kill all sessions in correct order 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
@@ -10,6 +11,11 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
shutdownGraceful bool
|
||||||
|
shutdownWait int
|
||||||
|
)
|
||||||
|
|
||||||
var startCmd = &cobra.Command{
|
var startCmd = &cobra.Command{
|
||||||
Use: "start",
|
Use: "start",
|
||||||
Short: "Start Gas Town",
|
Short: "Start Gas Town",
|
||||||
@@ -29,16 +35,22 @@ var shutdownCmd = &cobra.Command{
|
|||||||
Short: "Shutdown Gas Town",
|
Short: "Shutdown Gas Town",
|
||||||
Long: `Shutdown Gas Town by stopping all agents.
|
Long: `Shutdown Gas Town by stopping all agents.
|
||||||
|
|
||||||
|
By default, immediately kills all sessions. Use --graceful to allow agents
|
||||||
|
time to save their state and update handoff beads.
|
||||||
|
|
||||||
Stops agents in the correct order:
|
Stops agents in the correct order:
|
||||||
1. Deacon (health monitor) - so it doesn't restart others
|
1. Deacon (health monitor) - so it doesn't restart others
|
||||||
2. All polecats, witnesses, refineries, crew
|
2. All polecats, witnesses, refineries, crew
|
||||||
3. Mayor (global coordinator)
|
3. Mayor (global coordinator)`,
|
||||||
|
|
||||||
This is a graceful shutdown that kills all Gas Town tmux sessions.`,
|
|
||||||
RunE: runShutdown,
|
RunE: runShutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
shutdownCmd.Flags().BoolVarP(&shutdownGraceful, "graceful", "g", false,
|
||||||
|
"Send ESC to agents and wait for them to handoff before killing")
|
||||||
|
shutdownCmd.Flags().IntVarP(&shutdownWait, "wait", "w", 30,
|
||||||
|
"Seconds to wait for graceful shutdown (default 30)")
|
||||||
|
|
||||||
rootCmd.AddCommand(startCmd)
|
rootCmd.AddCommand(startCmd)
|
||||||
rootCmd.AddCommand(shutdownCmd)
|
rootCmd.AddCommand(shutdownCmd)
|
||||||
}
|
}
|
||||||
@@ -91,6 +103,106 @@ func runStart(cmd *cobra.Command, args []string) error {
|
|||||||
func runShutdown(cmd *cobra.Command, args []string) error {
|
func runShutdown(cmd *cobra.Command, args []string) error {
|
||||||
t := tmux.NewTmux()
|
t := tmux.NewTmux()
|
||||||
|
|
||||||
|
if shutdownGraceful {
|
||||||
|
return runGracefulShutdown(t)
|
||||||
|
}
|
||||||
|
return runImmediateShutdown(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGracefulShutdown(t *tmux.Tmux) error {
|
||||||
|
fmt.Printf("Graceful shutdown of Gas Town (waiting up to %ds)...\n\n", shutdownWait)
|
||||||
|
|
||||||
|
// Collect all gt-* sessions
|
||||||
|
sessions, err := t.ListSessions()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing sessions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gtSessions []string
|
||||||
|
for _, sess := range sessions {
|
||||||
|
if strings.HasPrefix(sess, "gt-") {
|
||||||
|
gtSessions = append(gtSessions, sess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gtSessions) == 0 {
|
||||||
|
fmt.Printf("%s Gas Town was not running\n", style.Dim.Render("○"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: Send ESC to all agents to interrupt them
|
||||||
|
fmt.Printf("Phase 1: Sending ESC to %d agent(s)...\n", len(gtSessions))
|
||||||
|
for _, sess := range gtSessions {
|
||||||
|
fmt.Printf(" %s Interrupting %s\n", style.Bold.Render("→"), sess)
|
||||||
|
_ = t.SendKeysRaw(sess, "Escape")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Send shutdown message asking agents to handoff
|
||||||
|
fmt.Printf("\nPhase 2: Requesting handoff from agents...\n")
|
||||||
|
shutdownMsg := "[SHUTDOWN] Gas Town is shutting down. Please save your state and update your handoff bead, then type /exit or wait to be terminated."
|
||||||
|
for _, sess := range gtSessions {
|
||||||
|
// Small delay then send the message
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
_ = t.SendKeys(sess, shutdownMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Wait for agents to complete handoff
|
||||||
|
fmt.Printf("\nPhase 3: Waiting %ds for agents to complete handoff...\n", shutdownWait)
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("(Press Ctrl-C to force immediate shutdown)"))
|
||||||
|
|
||||||
|
// Wait with countdown
|
||||||
|
for remaining := shutdownWait; remaining > 0; remaining -= 5 {
|
||||||
|
if remaining < shutdownWait {
|
||||||
|
fmt.Printf(" %s %ds remaining...\n", style.Dim.Render("⏳"), remaining)
|
||||||
|
}
|
||||||
|
sleepTime := 5
|
||||||
|
if remaining < 5 {
|
||||||
|
sleepTime = remaining
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Kill all sessions
|
||||||
|
fmt.Printf("\nPhase 4: Terminating sessions...\n")
|
||||||
|
stopped := 0
|
||||||
|
|
||||||
|
// Stop Deacon first
|
||||||
|
for _, sess := range gtSessions {
|
||||||
|
if sess == DeaconSessionName {
|
||||||
|
if err := t.KillSession(sess); err == nil {
|
||||||
|
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), sess)
|
||||||
|
stopped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop others (except Mayor)
|
||||||
|
for _, sess := range gtSessions {
|
||||||
|
if sess == DeaconSessionName || sess == MayorSessionName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := t.KillSession(sess); err == nil {
|
||||||
|
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), sess)
|
||||||
|
stopped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop Mayor last
|
||||||
|
for _, sess := range gtSessions {
|
||||||
|
if sess == MayorSessionName {
|
||||||
|
if err := t.KillSession(sess); err == nil {
|
||||||
|
fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), sess)
|
||||||
|
stopped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s Graceful shutdown complete (%d sessions stopped)\n", style.Bold.Render("✓"), stopped)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runImmediateShutdown(t *tmux.Tmux) error {
|
||||||
fmt.Println("Shutting down Gas Town...\n")
|
fmt.Println("Shutting down Gas Town...\n")
|
||||||
|
|
||||||
stopped := 0
|
stopped := 0
|
||||||
|
|||||||
Reference in New Issue
Block a user