feat(lifecycle): immediate daemon notification via SIGUSR1
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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)
|
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
|
// Set requesting state
|
||||||
if err := setRequestingState(role, action, townRoot); err != nil {
|
if err := setRequestingState(role, action, townRoot); err != nil {
|
||||||
fmt.Printf("Warning: failed to set state: %v\n", err)
|
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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ func runMailCheck(cmd *cobra.Command, args []string) error {
|
|||||||
messages, _ := mailbox.ListUnread()
|
messages, _ := mailbox.ListUnread()
|
||||||
var subjects []string
|
var subjects []string
|
||||||
for _, msg := range messages {
|
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("<system-reminder>")
|
fmt.Println("<system-reminder>")
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (d *Daemon) Run() error {
|
|||||||
|
|
||||||
// Handle signals
|
// Handle signals
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
||||||
|
|
||||||
// Heartbeat ticker
|
// Heartbeat ticker
|
||||||
ticker := time.NewTicker(d.config.HeartbeatInterval)
|
ticker := time.NewTicker(d.config.HeartbeatInterval)
|
||||||
@@ -103,8 +103,14 @@ func (d *Daemon) Run() error {
|
|||||||
return d.shutdown(state)
|
return d.shutdown(state)
|
||||||
|
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
|
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)
|
d.logger.Printf("Received signal %v, shutting down", sig)
|
||||||
return d.shutdown(state)
|
return d.shutdown(state)
|
||||||
|
}
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
d.heartbeat(state)
|
d.heartbeat(state)
|
||||||
|
|||||||
Reference in New Issue
Block a user