Add session beacon for predecessor discovery via /resume
Inject an identity beacon as the first message when Gas Town starts Claude sessions. This beacon becomes the session title in Claude Code '/resume picker, enabling workers to find their predecessor sessions for debugging. Beacon format: [GAS TOWN] <address> • <mol-id or "ready"> • <timestamp> Examples: - [GAS TOWN] gastown/crew/max • gt-abc12 • 2025-12-30T14:32 - [GAS TOWN] gastown/polecats/Toast • ready • 2025-12-30T09:15 - [GAS TOWN] deacon • patrol • 2025-12-30T08:00 Workers can now press / in /resume picker and search for their address (e.g., "gastown/crew/max") to see all predecessor sessions. Note: Respawn-loop agents (deacon/refinery via up.go) skip beacon injection since Claude restarts multiple times - would need post-restart injection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/crew"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
@@ -181,6 +182,18 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("starting claude: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start
|
||||
shells := constants.SupportedShells
|
||||
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
_ = t.NudgeSession(sessionID, beacon) // Non-fatal
|
||||
|
||||
fmt.Printf("%s Refreshed crew workspace: %s/%s\n",
|
||||
style.Bold.Render("✓"), r.Name, name)
|
||||
fmt.Printf("Attach with: %s\n", style.Dim.Render(fmt.Sprintf("gt crew at %s", name)))
|
||||
@@ -330,6 +343,14 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
// Give Claude time to initialize after process starts
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
if err := t.NudgeSession(sessionID, beacon); err != nil {
|
||||
// Non-fatal: session works without beacon
|
||||
}
|
||||
|
||||
if err := t.SendKeys(sessionID, "gt prime"); err != nil {
|
||||
// Non-fatal: Claude started but priming failed
|
||||
style.PrintWarning("Could not send prime command to %s: %v", arg, err)
|
||||
@@ -495,6 +516,12 @@ func restartCrewSession(rigName, crewName, clonePath string) error {
|
||||
// Non-fatal warning
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
_ = t.NudgeSession(sessionID, beacon) // Non-fatal
|
||||
|
||||
if err := t.SendKeys(sessionID, "gt prime"); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/deacon"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -197,6 +199,16 @@ func startDeaconSession(t *tmux.Tmux) error {
|
||||
return fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(DeaconSessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
beacon := session.SessionBeacon("deacon", "patrol")
|
||||
_ = t.NudgeSession(DeaconSessionName, beacon) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -135,6 +137,16 @@ func startMayorSession(t *tmux.Tmux) error {
|
||||
return fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(MayorSessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
beacon := session.SessionBeacon("mayor", "")
|
||||
_ = t.NudgeSession(MayorSessionName, beacon) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -797,6 +798,13 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
|
||||
// Give Claude time to initialize after process starts
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", rigName, name)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
if err := t.NudgeSession(sessionID, beacon); err != nil {
|
||||
// Non-fatal: session works without beacon
|
||||
}
|
||||
|
||||
// Send gt prime to initialize context
|
||||
if err := t.SendKeys(sessionID, "gt prime"); err != nil {
|
||||
style.PrintWarning("Could not send prime command: %v", err)
|
||||
@@ -931,6 +939,11 @@ func startCrewMember(rigName, crewName, townRoot string) error {
|
||||
// Give Claude time to initialize
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
_ = t.NudgeSession(sessionID, beacon) // Non-fatal
|
||||
|
||||
// Send gt prime to initialize context (non-fatal: session works without priming)
|
||||
_ = t.SendKeys(sessionID, "gt prime")
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"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/daemon"
|
||||
"github.com/steveyegge/gastown/internal/events"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -271,6 +273,20 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
// Note: Deacon respawn loop makes beacon tricky - Claude restarts multiple times
|
||||
// For non-respawn (mayor), inject beacon
|
||||
if role != "deacon" {
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
beacon := session.SessionBeacon(role, "")
|
||||
_ = t.NudgeSession(sessionName, beacon) // Non-fatal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -306,6 +322,17 @@ func ensureWitness(t *tmux.Tmux, sessionName, rigPath, rigName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/witness", rigName)
|
||||
beacon := session.SessionBeacon(address, "patrol")
|
||||
_ = t.NudgeSession(sessionName, beacon) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -517,6 +544,17 @@ func ensureCrewSession(t *tmux.Tmux, sessionName, crewPath, rigName, crewName st
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
_ = t.NudgeSession(sessionName, beacon) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -605,5 +643,16 @@ func ensurePolecatSession(t *tmux.Tmux, sessionName, polecatPath, rigName, polec
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/polecats/%s", rigName, polecatName)
|
||||
beacon := session.SessionBeacon(address, "")
|
||||
_ = t.NudgeSession(sessionName, beacon) // Non-fatal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/witness"
|
||||
@@ -333,6 +336,17 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
|
||||
return false, fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal)
|
||||
if err := t.WaitForCommand(sessionName, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
address := fmt.Sprintf("%s/witness", rigName)
|
||||
beacon := session.SessionBeacon(address, "patrol")
|
||||
_ = t.NudgeSession(sessionName, beacon) // Non-fatal
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/claude"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
@@ -185,6 +186,21 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
return fmt.Errorf("sending command: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Claude to start (non-fatal: session continues even if this times out)
|
||||
if err := m.tmux.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
|
||||
// Non-fatal warning - Claude might still start
|
||||
}
|
||||
time.Sleep(constants.ShutdownNotifyDelay)
|
||||
|
||||
// Inject session beacon for predecessor discovery via /resume
|
||||
// This becomes the session title in Claude Code's session picker
|
||||
address := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat)
|
||||
molID := opts.Issue // Use issue ID if provided
|
||||
beacon := SessionBeacon(address, molID)
|
||||
if err := m.tmux.NudgeSession(sessionID, beacon); err != nil {
|
||||
// Non-fatal: session works without beacon
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Package session provides polecat session lifecycle management.
|
||||
package session
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Prefix is the common prefix for all Gas Town tmux session names.
|
||||
const Prefix = "gt-"
|
||||
@@ -35,3 +38,22 @@ func CrewSessionName(rig, name string) string {
|
||||
func PolecatSessionName(rig, name string) string {
|
||||
return fmt.Sprintf("%s%s-%s", Prefix, rig, name)
|
||||
}
|
||||
|
||||
// SessionBeacon generates an identity beacon message for Claude Code sessions.
|
||||
// This beacon becomes the session title in /resume picker, enabling workers to
|
||||
// find their predecessor sessions.
|
||||
//
|
||||
// Format: [GAS TOWN] <address> • <mol-id or "ready"> • <timestamp>
|
||||
//
|
||||
// Examples:
|
||||
// - [GAS TOWN] gastown/crew/max • gt-abc12 • 2025-12-30T14:32
|
||||
// - [GAS TOWN] gastown/polecats/Toast • ready • 2025-12-30T09:15
|
||||
// - [GAS TOWN] deacon • patrol • 2025-12-30T08:00
|
||||
func SessionBeacon(address, molID string) string {
|
||||
if molID == "" {
|
||||
molID = "ready"
|
||||
}
|
||||
// Use local time in a compact format
|
||||
timestamp := time.Now().Format("2006-01-02T15:04")
|
||||
return fmt.Sprintf("[GAS TOWN] %s • %s • %s", address, molID, timestamp)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user