feat(deacon): improve timing and add heartbeat command

Timing changes for more relaxed poke intervals:
- Daemon heartbeat: 60s → 5 minutes
- Backoff base: 60s → 5 minutes
- Backoff max: 10m → 30 minutes
- Fresh threshold: <2min → <5min
- Stale threshold: 2-5min → 5-15min
- Very stale threshold: >5min → >15min

New command:
- `gt deacon heartbeat [action]` - Touch heartbeat file easily

Template rewrite:
- Clearer wake/sleep model
- Documents wake sources (daemon poke, mail, timer callbacks)
- Simpler rounds with `gt deacon heartbeat` instead of bash echo
- Mentions plugins as optional maintenance tasks
- Explains timer callbacks pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-20 02:12:21 -08:00
parent 348a7d0525
commit 1554380228
9 changed files with 195 additions and 161 deletions

View File

@@ -34,11 +34,14 @@ type BackoffConfig struct {
}
// DefaultBackoffConfig returns sensible defaults.
// Base interval is 5 minutes since deacon rounds may take a while
// (health checks, plugins, syncing clones, complex remediation).
// Max interval is 30 minutes - beyond that, something is likely wrong.
func DefaultBackoffConfig() *BackoffConfig {
return &BackoffConfig{
Strategy: StrategyGeometric,
BaseInterval: 60 * time.Second,
MaxInterval: 10 * time.Minute,
BaseInterval: 5 * time.Minute,
MaxInterval: 30 * time.Minute,
Factor: 1.5,
}
}

View File

@@ -11,11 +11,11 @@ func TestDefaultBackoffConfig(t *testing.T) {
if config.Strategy != StrategyGeometric {
t.Errorf("expected strategy Geometric, got %v", config.Strategy)
}
if config.BaseInterval != 60*time.Second {
t.Errorf("expected base interval 60s, got %v", config.BaseInterval)
if config.BaseInterval != 5*time.Minute {
t.Errorf("expected base interval 5m, got %v", config.BaseInterval)
}
if config.MaxInterval != 10*time.Minute {
t.Errorf("expected max interval 10m, got %v", config.MaxInterval)
if config.MaxInterval != 30*time.Minute {
t.Errorf("expected max interval 30m, got %v", config.MaxInterval)
}
if config.Factor != 1.5 {
t.Errorf("expected factor 1.5, got %v", config.Factor)
@@ -29,11 +29,11 @@ func TestNewAgentBackoff(t *testing.T) {
if ab.AgentID != "test-agent" {
t.Errorf("expected agent ID 'test-agent', got %s", ab.AgentID)
}
if ab.BaseInterval != 60*time.Second {
t.Errorf("expected base interval 60s, got %v", ab.BaseInterval)
if ab.BaseInterval != 5*time.Minute {
t.Errorf("expected base interval 5m, got %v", ab.BaseInterval)
}
if ab.CurrentInterval != 60*time.Second {
t.Errorf("expected current interval 60s, got %v", ab.CurrentInterval)
if ab.CurrentInterval != 5*time.Minute {
t.Errorf("expected current interval 5m, got %v", ab.CurrentInterval)
}
if ab.ConsecutiveMiss != 0 {
t.Errorf("expected consecutive miss 0, got %d", ab.ConsecutiveMiss)

View File

@@ -219,7 +219,7 @@ func (d *Daemon) pokeDeacon() {
}
// Send heartbeat message via tmux
msg := "HEARTBEAT: check Mayor and Witnesses"
msg := "HEARTBEAT: run your rounds"
if err := d.tmux.SendKeys(DeaconSessionName, msg); err != nil {
d.logger.Printf("Error poking Deacon: %v", err)
return

View File

@@ -12,8 +12,8 @@ func TestDefaultConfig(t *testing.T) {
townRoot := "/tmp/test-town"
config := DefaultConfig(townRoot)
if config.HeartbeatInterval != 60*time.Second {
t.Errorf("expected HeartbeatInterval 60s, got %v", config.HeartbeatInterval)
if config.HeartbeatInterval != 5*time.Minute {
t.Errorf("expected HeartbeatInterval 5m, got %v", config.HeartbeatInterval)
}
if config.TownRoot != townRoot {
t.Errorf("expected TownRoot %q, got %q", townRoot, config.TownRoot)

View File

@@ -34,7 +34,7 @@ type Config struct {
func DefaultConfig(townRoot string) *Config {
daemonDir := filepath.Join(townRoot, "daemon")
return &Config{
HeartbeatInterval: 60 * time.Second,
HeartbeatInterval: 5 * time.Minute, // Deacon wakes on mail too, no need to poke often
TownRoot: townRoot,
LogFile: filepath.Join(daemonDir, "daemon.log"),
PidFile: filepath.Join(daemonDir, "daemon.pid"),