fix(sling): Wait for Claude to be ready before nudging existing sessions (#146)
When `gt sling` targets an existing polecat session, it now waits for Claude to be ready before sending the nudge message. This fixes issue #115 where the "Work slung" message would arrive before Claude had fully started. Changes: - Add getSessionFromPane() to extract session name from pane target - Add ensureClaudeReady() to wait for Claude startup using the same pragmatic approach as session.Start() (poll for node, accept bypass dialog, then 8-second delay) - Call ensureClaudeReady() before injectStartPrompt() in runSling() The fix uses IsClaudeRunning() for a fast path when Claude is already running, avoiding unnecessary delays for sessions that have been running for a while. Fixes #115 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
ad6169201a
commit
87a2e27fcc
@@ -9,10 +9,12 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/dog"
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
@@ -451,12 +453,24 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
// Try to inject the "start now" prompt (graceful if no tmux)
|
||||
if targetPane == "" {
|
||||
fmt.Printf("%s No pane to nudge (agent will discover work via gt prime)\n", style.Dim.Render("○"))
|
||||
} else if err := injectStartPrompt(targetPane, beadID, slingSubject, slingArgs); err != nil {
|
||||
// Graceful fallback for no-tmux mode
|
||||
fmt.Printf("%s Could not nudge (no tmux?): %v\n", style.Dim.Render("○"), err)
|
||||
fmt.Printf(" Agent will discover work via gt prime / bd show\n")
|
||||
} else {
|
||||
fmt.Printf("%s Start prompt sent\n", style.Bold.Render("▶"))
|
||||
// Ensure Claude 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 {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := injectStartPrompt(targetPane, beadID, slingSubject, slingArgs); err != nil {
|
||||
// Graceful fallback for no-tmux mode
|
||||
fmt.Printf("%s Could not nudge (no tmux?): %v\n", style.Dim.Render("○"), err)
|
||||
fmt.Printf(" Agent will discover work via gt prime / bd show\n")
|
||||
} else {
|
||||
fmt.Printf("%s Start prompt sent\n", style.Bold.Render("▶"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -577,6 +591,55 @@ func injectStartPrompt(pane, beadID, subject, args string) error {
|
||||
return t.NudgePane(pane, prompt)
|
||||
}
|
||||
|
||||
// getSessionFromPane extracts session name from a pane target.
|
||||
// Pane targets can be:
|
||||
// - "%9" (pane ID) - need to query tmux for session
|
||||
// - "gt-rig-name:0.0" (session:window.pane) - extract session name
|
||||
func getSessionFromPane(pane string) string {
|
||||
if strings.HasPrefix(pane, "%") {
|
||||
// Pane ID format - query tmux for the session
|
||||
cmd := exec.Command("tmux", "display-message", "-t", pane, "-p", "#{session_name}")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
// Session:window.pane format - extract session name
|
||||
if idx := strings.Index(pane, ":"); idx > 0 {
|
||||
return pane[:idx]
|
||||
}
|
||||
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 {
|
||||
t := tmux.NewTmux()
|
||||
|
||||
// If Claude is already running, assume it's ready (session was started earlier)
|
||||
if t.IsClaudeRunning(sessionName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Claude not running yet - wait for it to start (shell → node transition)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
return fmt.Errorf("waiting for Claude to start: %w", err)
|
||||
}
|
||||
|
||||
// Accept bypass permissions warning if present
|
||||
_ = 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)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveTargetAgent converts a target spec to agent ID, pane, and hook root.
|
||||
// If skipPane is true, skip tmux pane lookup (for --naked mode).
|
||||
func resolveTargetAgent(target string, skipPane bool) (agentID string, pane string, hookRoot string, err error) {
|
||||
|
||||
Reference in New Issue
Block a user