Fix spawn priming race: use deacon patrol instead of broken nudge (gt-6957)
- Remove 3-second sleep and NudgeSession from spawn.go (Claude takes 10-20s to initialize, message arrived too early) - Add trigger-pending-spawns step to deacon patrol molecule (Deacon polls with WaitForClaudeReady, sends trigger when ready) - SessionStart hook handles gt prime (internal, reliable) - Deacon sends minimal 'Begin.' trigger to start polecat working The spawn command now returns immediately after starting the session. The deacon's patrol cycle will trigger the polecat when it's ready.
This commit is contained in:
@@ -632,6 +632,31 @@ gt mail read <id>
|
|||||||
|
|
||||||
Callbacks may spawn new polecats, update issue state, or trigger other actions.
|
Callbacks may spawn new polecats, update issue state, or trigger other actions.
|
||||||
|
|
||||||
|
## Step: trigger-pending-spawns
|
||||||
|
Nudge newly spawned polecats that are ready for input.
|
||||||
|
|
||||||
|
When polecats are spawned, their Claude session takes 10-20 seconds to initialize.
|
||||||
|
The spawn command returns immediately without waiting. This step finds spawned
|
||||||
|
polecats that are now ready and sends them a trigger to start working.
|
||||||
|
|
||||||
|
` + "```" + `bash
|
||||||
|
# For each rig with polecats
|
||||||
|
for rig in gastown beads; do
|
||||||
|
gt polecats $rig
|
||||||
|
# For each working polecat, check if Claude is ready
|
||||||
|
# Use tmux capture-pane to look for "> " prompt
|
||||||
|
done
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
For each ready polecat that hasn't been triggered yet:
|
||||||
|
1. Send "Begin." to trigger UserPromptSubmit hook
|
||||||
|
2. The hook injects mail, polecat sees its assignment
|
||||||
|
3. Mark polecat as triggered in state
|
||||||
|
|
||||||
|
Use WaitForClaudeReady from tmux package (polls for "> " prompt).
|
||||||
|
Timeout: 60 seconds per polecat. If not ready, try again next cycle.
|
||||||
|
Needs: inbox-check
|
||||||
|
|
||||||
## Step: health-scan
|
## Step: health-scan
|
||||||
Ping Witnesses and Refineries.
|
Ping Witnesses and Refineries.
|
||||||
|
|
||||||
@@ -649,7 +674,7 @@ done
|
|||||||
` + "```" + `
|
` + "```" + `
|
||||||
|
|
||||||
Report any issues found. Restart unresponsive components if needed.
|
Report any issues found. Restart unresponsive components if needed.
|
||||||
Needs: inbox-check
|
Needs: trigger-pending-spawns
|
||||||
|
|
||||||
## Step: plugin-run
|
## Step: plugin-run
|
||||||
Execute registered plugins.
|
Execute registered plugins.
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
@@ -354,41 +353,30 @@ func runSpawn(cmd *cobra.Command, args []string) error {
|
|||||||
// Check if already running
|
// Check if already running
|
||||||
running, _ := sessMgr.IsRunning(polecatName)
|
running, _ := sessMgr.IsRunning(polecatName)
|
||||||
if running {
|
if running {
|
||||||
// Session already running - send notification to check inbox
|
fmt.Printf("Session already running\n")
|
||||||
fmt.Printf("Session already running, notifying to check inbox...\n")
|
|
||||||
time.Sleep(500 * time.Millisecond) // Brief pause for notification
|
|
||||||
} else {
|
} else {
|
||||||
// Start new session
|
// Start new session
|
||||||
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
fmt.Printf("Starting session for %s/%s...\n", rigName, polecatName)
|
||||||
if err := sessMgr.Start(polecatName, session.StartOptions{}); err != nil {
|
if err := sessMgr.Start(polecatName, session.StartOptions{}); err != nil {
|
||||||
return fmt.Errorf("starting session: %w", err)
|
return fmt.Errorf("starting session: %w", err)
|
||||||
}
|
}
|
||||||
// Wait for Claude Code to fully initialize (banner, prompt ready)
|
|
||||||
// 3 seconds is enough for the UI to stabilize
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Session started. Attach with: %s\n",
|
fmt.Printf("%s Session started. Attach with: %s\n",
|
||||||
style.Bold.Render("✓"),
|
style.Bold.Render("✓"),
|
||||||
style.Dim.Render(fmt.Sprintf("gt session at %s/%s", rigName, polecatName)))
|
style.Dim.Render(fmt.Sprintf("gt session at %s/%s", rigName, polecatName)))
|
||||||
|
|
||||||
// Send direct nudge to start working using reliable NudgeSession
|
// NOTE: We do NOT send a nudge here. Claude Code takes 10-20+ seconds to initialize,
|
||||||
// The polecat has a work assignment in its inbox; just tell it to check
|
// and sending keys before the prompt is ready causes them to be mangled.
|
||||||
sessionName := sessMgr.SessionName(polecatName)
|
// The Deacon will poll with WaitForClaudeReady and send a trigger when ready.
|
||||||
nudgeMsg := fmt.Sprintf("You have a work assignment. Run 'gt mail inbox' to see it, then start working on issue %s.", assignmentID)
|
// The polecat's SessionStart hook runs gt prime, and work assignment is in its inbox.
|
||||||
if err := t.NudgeSession(sessionName, nudgeMsg); err != nil {
|
|
||||||
fmt.Printf(" %s\n", style.Dim.Render(fmt.Sprintf("Warning: could not nudge polecat: %v", err)))
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" %s\n", style.Dim.Render("Polecat nudged to start working"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify Witness about the spawn - Witness will monitor startup and nudge when ready
|
// Notify Witness about the spawn for monitoring
|
||||||
// Note: If Witness is down, Deacon's health check will wake it and Witness will
|
|
||||||
// process the SPAWN message from its inbox on startup.
|
|
||||||
// Use town-level beads for cross-agent mail (gt-c6b: mail coordination uses town-level)
|
// Use town-level beads for cross-agent mail (gt-c6b: mail coordination uses town-level)
|
||||||
townRouter := mail.NewRouter(townRoot)
|
townRouter := mail.NewRouter(townRoot)
|
||||||
witnessAddr := fmt.Sprintf("%s/witness", rigName)
|
witnessAddr := fmt.Sprintf("%s/witness", rigName)
|
||||||
sender := detectSender()
|
sender := detectSender()
|
||||||
|
sessionName := sessMgr.SessionName(polecatName)
|
||||||
spawnNotification := &mail.Message{
|
spawnNotification := &mail.Message{
|
||||||
To: witnessAddr,
|
To: witnessAddr,
|
||||||
From: sender,
|
From: sender,
|
||||||
@@ -400,12 +388,9 @@ Issue: %s
|
|||||||
Session: %s
|
Session: %s
|
||||||
Spawned by: %s
|
Spawned by: %s
|
||||||
|
|
||||||
Please monitor this polecat's startup. When Claude is ready (you can see the prompt
|
The Deacon will trigger this polecat when Claude is ready (WaitForClaudeReady).
|
||||||
in the tmux session), send a nudge to start working:
|
The polecat's SessionStart hook runs gt prime, and work assignment is in its inbox.
|
||||||
|
Monitor for stuck/idle state after a few minutes.`, polecatName, assignmentID, sessionName, sender),
|
||||||
tmux send-keys -t %s "Check your inbox with 'gt mail inbox' and begin working." Enter
|
|
||||||
|
|
||||||
The polecat has a work assignment in its inbox.`, polecatName, assignmentID, sessMgr.SessionName(polecatName), sender, sessMgr.SessionName(polecatName)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := townRouter.Send(spawnNotification); err != nil {
|
if err := townRouter.Send(spawnNotification); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user