Files
gastown/internal/daemon/types.go
Steve Yegge 1265df70f9 feat: add gt daemon for town-level background service
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>
2025-12-18 13:06:20 -08:00

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"`
}