diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 3de04256..1cae0121 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + "github.com/steveyegge/gastown/internal/polecat" "github.com/steveyegge/gastown/internal/tmux" ) @@ -119,7 +120,12 @@ func (d *Daemon) heartbeat(state *State) { // 2. Send heartbeat to Deacon (simple notification, no decision-making) d.pokeDeacon() - // 3. Process lifecycle requests + // 3. Trigger pending polecat spawns (bootstrap mode - ZFC violation acceptable) + // This ensures polecats get nudged even when Deacon isn't in a patrol cycle. + // Uses regex-based WaitForClaudeReady, which is acceptable for daemon bootstrap. + d.triggerPendingSpawns() + + // 4. Process lifecycle requests d.processLifecycleRequests() // Update state @@ -258,6 +264,55 @@ func (d *Daemon) pokeDeacon() { // The Deacon molecule is responsible for monitoring Mayor and Witnesses. // The daemon only ensures Deacon is running and sends it heartbeats. +// triggerPendingSpawns polls pending polecat spawns and triggers those that are ready. +// This is bootstrap mode - uses regex-based WaitForClaudeReady which is acceptable +// for daemon operations when no AI agent is guaranteed to be running. +// The timeout is short (2s) to avoid blocking the heartbeat. +func (d *Daemon) triggerPendingSpawns() { + const triggerTimeout = 2 * time.Second + + // Check for pending spawns (from POLECAT_STARTED messages in Deacon inbox) + pending, err := polecat.CheckInboxForSpawns(d.config.TownRoot) + if err != nil { + d.logger.Printf("Error checking pending spawns: %v", err) + return + } + + if len(pending) == 0 { + return + } + + d.logger.Printf("Found %d pending spawn(s), attempting to trigger...", len(pending)) + + // Trigger pending spawns (uses WaitForClaudeReady with short timeout) + results, err := polecat.TriggerPendingSpawns(d.config.TownRoot, triggerTimeout) + if err != nil { + d.logger.Printf("Error triggering spawns: %v", err) + return + } + + // Log results + triggered := 0 + for _, r := range results { + if r.Triggered { + triggered++ + d.logger.Printf("Triggered polecat: %s/%s", r.Spawn.Rig, r.Spawn.Polecat) + } else if r.Error != nil { + d.logger.Printf("Error triggering %s: %v", r.Spawn.Session, r.Error) + } + } + + if triggered > 0 { + d.logger.Printf("Triggered %d/%d pending spawn(s)", triggered, len(pending)) + } + + // Prune stale pending spawns (older than 5 minutes - likely dead sessions) + pruned, _ := polecat.PruneStalePending(d.config.TownRoot, 5*time.Minute) + if pruned > 0 { + d.logger.Printf("Pruned %d stale pending spawn(s)", pruned) + } +} + // processLifecycleRequests checks for and processes lifecycle requests. func (d *Daemon) processLifecycleRequests() { d.ProcessLifecycleRequests()