Implements the town daemon (gt-99m) that handles: - Periodic heartbeat to poke Mayor and Witnesses - Lifecycle request processing (cycle, restart, shutdown) - Session management for agent restarts Commands: - gt daemon start: Start daemon in background - gt daemon stop: Stop running daemon - gt daemon status: Show daemon status and stats - gt daemon logs: View daemon log file The daemon is a "dumb scheduler" - all intelligence remains in agents. It simply pokes them on schedule and executes lifecycle requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
127 lines
3.3 KiB
Go
127 lines
3.3 KiB
Go
// Package daemon provides the town-level background service for Gas Town.
|
|
//
|
|
// The daemon is a simple Go process (not a Claude agent) that:
|
|
// 1. Pokes agents periodically (heartbeat)
|
|
// 2. Processes lifecycle requests (cycle, restart, shutdown)
|
|
// 3. Restarts sessions when agents request cycling
|
|
//
|
|
// The daemon is a "dumb scheduler" - all intelligence is in agents.
|
|
package daemon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Config holds daemon configuration.
|
|
type Config struct {
|
|
// HeartbeatInterval is how often to poke agents.
|
|
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
|
|
|
|
// TownRoot is the Gas Town workspace root.
|
|
TownRoot string `json:"town_root"`
|
|
|
|
// LogFile is the path to the daemon log file.
|
|
LogFile string `json:"log_file"`
|
|
|
|
// PidFile is the path to the PID file.
|
|
PidFile string `json:"pid_file"`
|
|
}
|
|
|
|
// DefaultConfig returns the default daemon configuration.
|
|
func DefaultConfig(townRoot string) *Config {
|
|
daemonDir := filepath.Join(townRoot, "daemon")
|
|
return &Config{
|
|
HeartbeatInterval: 60 * time.Second,
|
|
TownRoot: townRoot,
|
|
LogFile: filepath.Join(daemonDir, "daemon.log"),
|
|
PidFile: filepath.Join(daemonDir, "daemon.pid"),
|
|
}
|
|
}
|
|
|
|
// State represents the daemon's runtime state.
|
|
type State struct {
|
|
// Running indicates if the daemon is running.
|
|
Running bool `json:"running"`
|
|
|
|
// PID is the process ID of the daemon.
|
|
PID int `json:"pid"`
|
|
|
|
// StartedAt is when the daemon started.
|
|
StartedAt time.Time `json:"started_at"`
|
|
|
|
// LastHeartbeat is when the last heartbeat completed.
|
|
LastHeartbeat time.Time `json:"last_heartbeat"`
|
|
|
|
// HeartbeatCount is how many heartbeats have completed.
|
|
HeartbeatCount int64 `json:"heartbeat_count"`
|
|
}
|
|
|
|
// StateFile returns the path to the state file.
|
|
func StateFile(townRoot string) string {
|
|
return filepath.Join(townRoot, "daemon", "state.json")
|
|
}
|
|
|
|
// LoadState loads daemon state from disk.
|
|
func LoadState(townRoot string) (*State, error) {
|
|
stateFile := StateFile(townRoot)
|
|
data, err := os.ReadFile(stateFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return &State{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var state State
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
return nil, err
|
|
}
|
|
return &state, nil
|
|
}
|
|
|
|
// SaveState saves daemon state to disk.
|
|
func SaveState(townRoot string, state *State) error {
|
|
stateFile := StateFile(townRoot)
|
|
|
|
// Ensure daemon directory exists
|
|
if err := os.MkdirAll(filepath.Dir(stateFile), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := json.MarshalIndent(state, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(stateFile, data, 0644)
|
|
}
|
|
|
|
// LifecycleAction represents a lifecycle request action.
|
|
type LifecycleAction string
|
|
|
|
const (
|
|
// ActionCycle restarts the session with handoff.
|
|
ActionCycle LifecycleAction = "cycle"
|
|
|
|
// ActionRestart does a fresh restart without handoff.
|
|
ActionRestart LifecycleAction = "restart"
|
|
|
|
// ActionShutdown terminates without restart.
|
|
ActionShutdown LifecycleAction = "shutdown"
|
|
)
|
|
|
|
// LifecycleRequest represents a request from an agent to the daemon.
|
|
type LifecycleRequest struct {
|
|
// From is the agent requesting the action (e.g., "mayor/", "gastown/witness").
|
|
From string `json:"from"`
|
|
|
|
// Action is what lifecycle action to perform.
|
|
Action LifecycleAction `json:"action"`
|
|
|
|
// Timestamp is when the request was made.
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|