feat: runtime-aware tmux agent checks

This commit is contained in:
jv
2026-01-07 12:56:00 +13:00
committed by Steve Yegge
parent 02ca9e43fa
commit 22693c1dcc
9 changed files with 117 additions and 46 deletions

View File

@@ -150,8 +150,8 @@ func runLiveCosts() error {
// Extract cost from content
cost := extractCost(content)
// Check if Claude is running
running := t.IsClaudeRunning(session)
// Check if an agent appears to be running
running := t.IsAgentRunning(session)
costs = append(costs, SessionCost{
Session: session,
@@ -428,7 +428,6 @@ func extractCost(content string) float64 {
return cost
}
func outputCostsJSON(output CostsOutput) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")

View File

@@ -89,9 +89,9 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
if !hasSession {
existingSessions, err := t.FindSessionByWorkDir(worker.ClonePath, true)
if err == nil && len(existingSessions) > 0 {
// Found an existing session with Claude running in this directory
// Found an existing session with an agent running in this directory
existingSession := existingSessions[0]
fmt.Printf("%s Found existing Claude session '%s' in crew directory\n",
fmt.Printf("%s Found existing agent session '%s' in crew directory\n",
style.Warning.Render("⚠"),
existingSession)
fmt.Printf(" Attaching to existing session instead of creating a new one\n")
@@ -164,7 +164,11 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
// Session exists - check if Claude is still running
// Uses both pane command check and UI marker detection to avoid
// restarting when user is in a subshell spawned from Claude
if !t.IsClaudeRunning(sessionID) {
agentCfg, _, err := config.ResolveAgentConfigWithOverride(townRoot, r.Path, crewAgentOverride)
if err != nil {
return fmt.Errorf("resolving agent: %w", err)
}
if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) {
// Claude has exited, restart it using respawn-pane
fmt.Printf("Claude exited, restarting...\n")

View File

@@ -447,13 +447,13 @@ func runSling(cmd *cobra.Command, args []string) error {
if targetPane == "" {
fmt.Printf("%s No pane to nudge (agent will discover work via gt prime)\n", style.Dim.Render("○"))
} else {
// Ensure Claude is ready before nudging (prevents race condition where
// Ensure agent is ready before nudging (prevents race condition where
// message arrives before Claude has fully started - see issue #115)
sessionName := getSessionFromPane(targetPane)
if sessionName != "" {
if err := ensureClaudeReady(sessionName); err != nil {
if err := ensureAgentReady(sessionName); err != nil {
// Non-fatal: warn and continue, agent will discover work via gt prime
fmt.Printf("%s Could not verify Claude ready: %v\n", style.Dim.Render("○"), err)
fmt.Printf("%s Could not verify agent ready: %v\n", style.Dim.Render("○"), err)
}
}
@@ -605,30 +605,32 @@ func getSessionFromPane(pane string) string {
return pane
}
// ensureClaudeReady waits for Claude to be ready before nudging an existing session.
// Uses the same pragmatic approach as session.Start(): poll for node process,
// accept bypass dialog if present, then wait for full initialization.
// Returns early if Claude is already running and ready.
func ensureClaudeReady(sessionName string) error {
// ensureAgentReady waits for an agent to be ready before nudging an existing session.
// Uses a pragmatic approach: wait for the pane to leave a shell, then (Claude-only)
// accept the bypass permissions warning and give it a moment to finish initializing.
func ensureAgentReady(sessionName string) error {
t := tmux.NewTmux()
// If Claude is already running, assume it's ready (session was started earlier)
if t.IsClaudeRunning(sessionName) {
// If an agent is already running, assume it's ready (session was started earlier)
if t.IsAgentRunning(sessionName) {
return nil
}
// Claude not running yet - wait for it to start (shell → node transition)
// Agent not running yet - wait for it to start (shell → program transition)
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
return fmt.Errorf("waiting for Claude to start: %w", err)
return fmt.Errorf("waiting for agent to start: %w", err)
}
// Accept bypass permissions warning if present
_ = t.AcceptBypassPermissionsWarning(sessionName)
// Claude-only: accept bypass permissions warning if present
if t.IsClaudeRunning(sessionName) {
_ = t.AcceptBypassPermissionsWarning(sessionName)
// Wait for Claude to be fully ready at the prompt
// PRAGMATIC APPROACH: Use fixed delay rather than detection.
// Claude startup takes ~5-8 seconds on typical machines.
time.Sleep(8 * time.Second)
// PRAGMATIC APPROACH: fixed delay rather than prompt detection.
// Claude startup takes ~5-8 seconds on typical machines.
time.Sleep(8 * time.Second)
} else {
time.Sleep(1 * time.Second)
}
return nil
}

View File

@@ -277,7 +277,8 @@ func startConfiguredCrew(t *tmux.Tmux, townRoot string) {
sessionID := crewSessionName(r.Name, crewName)
if running, _ := t.HasSession(sessionID); running {
// Session exists - check if Claude is still running
if !t.IsClaudeRunning(sessionID) {
agentCfg := config.ResolveAgentConfig(townRoot, r.Path)
if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) {
// Claude has exited, restart it
fmt.Printf(" %s %s/%s session exists, restarting Claude...\n", style.Dim.Render("○"), r.Name, crewName)
claudeCmd := config.BuildCrewStartupCommand(r.Name, crewName, r.Path, "gt prime")
@@ -800,7 +801,11 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
if hasSession {
// Session exists - check if Claude is still running
if !t.IsClaudeRunning(sessionID) {
agentCfg, _, err := config.ResolveAgentConfigWithOverride(townRoot, r.Path, startCrewAgentOverride)
if err != nil {
return fmt.Errorf("resolving agent: %w", err)
}
if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) {
// Claude has exited, restart it with "gt prime" as initial prompt
fmt.Printf("Session exists, restarting Claude...\n")
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(rigName, name, r.Path, "gt prime", startCrewAgentOverride)