From e5c88ae9d4920e3c892ecdf0afc00beffd463b95 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 20 Dec 2025 20:28:36 -0800 Subject: [PATCH] feat(lifecycle): immediate daemon notification via SIGUSR1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - daemon.go: Add SIGUSR1 handler to process lifecycle requests immediately - handoff.go: Signal daemon after sending lifecycle mail to deacon/ - mail.go: Show message IDs in hook output for direct reading Previously, gt handoff would wait up to 5 min for daemon heartbeat poll. Now lifecycle requests are processed within milliseconds. Closes gt-zut3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/handoff.go | 37 +++++++++++++++++++++++++++++++++++++ internal/cmd/mail.go | 2 +- internal/daemon/daemon.go | 12 +++++++++--- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index a07647ff..5a8d1d8a 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -6,7 +6,9 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" + "syscall" "time" "github.com/spf13/cobra" @@ -111,6 +113,16 @@ func runHandoff(cmd *cobra.Command, args []string) error { } fmt.Printf("%s Sent %s request to %s\n", style.Bold.Render("✓"), action, manager) + // Signal daemon for immediate processing (if manager is deacon) + if manager == "deacon/" { + if err := signalDaemon(townRoot); err != nil { + // Non-fatal: daemon will eventually poll + fmt.Printf("%s Could not signal daemon (will poll): %v\n", style.Dim.Render("○"), err) + } else { + fmt.Printf("%s Signaled daemon for immediate processing\n", style.Bold.Render("✓")) + } + } + // Set requesting state if err := setRequestingState(role, action, townRoot); err != nil { fmt.Printf("Warning: failed to set state: %v\n", err) @@ -357,3 +369,28 @@ func setRequestingState(role Role, action HandoffAction, townRoot string) error return os.WriteFile(stateFile, data, 0644) } + +// signalDaemon sends SIGUSR1 to the daemon to trigger immediate lifecycle processing. +func signalDaemon(townRoot string) error { + pidFile := filepath.Join(townRoot, "daemon", "daemon.pid") + data, err := os.ReadFile(pidFile) + if err != nil { + return fmt.Errorf("reading daemon PID: %w", err) + } + + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + if err != nil { + return fmt.Errorf("parsing daemon PID: %w", err) + } + + process, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("finding daemon process: %w", err) + } + + if err := process.Signal(syscall.SIGUSR1); err != nil { + return fmt.Errorf("signaling daemon: %w", err) + } + + return nil +} diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index 1c08bd86..dc4e1696 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -597,7 +597,7 @@ func runMailCheck(cmd *cobra.Command, args []string) error { messages, _ := mailbox.ListUnread() var subjects []string for _, msg := range messages { - subjects = append(subjects, fmt.Sprintf("- From %s: %s", msg.From, msg.Subject)) + subjects = append(subjects, fmt.Sprintf("- %s from %s: %s", msg.ID, msg.From, msg.Subject)) } fmt.Println("") diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 56a204ca..d230ec34 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -85,7 +85,7 @@ func (d *Daemon) Run() error { // Handle signals sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) // Heartbeat ticker ticker := time.NewTicker(d.config.HeartbeatInterval) @@ -103,8 +103,14 @@ func (d *Daemon) Run() error { return d.shutdown(state) case sig := <-sigChan: - d.logger.Printf("Received signal %v, shutting down", sig) - return d.shutdown(state) + if sig == syscall.SIGUSR1 { + // SIGUSR1: immediate lifecycle processing (from gt handoff) + d.logger.Println("Received SIGUSR1, processing lifecycle requests immediately") + d.processLifecycleRequests() + } else { + d.logger.Printf("Received signal %v, shutting down", sig) + return d.shutdown(state) + } case <-ticker.C: d.heartbeat(state)