heartbeat.go

59 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
//go:build cgo

package commands

import (
	"fmt"
	"os/exec"
	"strings"

	"congo.gg/dev/models"
)

// Heartbeat is the idempotent system evaluator. It checks the state
// of the system, writes a heartbeat to the DB, and corrects any drift.
// Called periodically by the persistent Claude agent.
func Heartbeat() {
	var status []string

	// Check containers
	if out, err := exec.Command("docker", "ps", "--format", "{{.Names}}: {{.Status}}").Output(); err == nil {
		lines := strings.Split(strings.TrimSpace(string(out)), "\n")
		healthy := 0
		for _, l := range lines {
			if strings.Contains(l, "healthy") || strings.Contains(l, "Up") {
				healthy++
			}
		}
		status = append(status, fmt.Sprintf("containers: %d/%d healthy", healthy, len(lines)))
	}

	// Check disk
	if out, err := exec.Command("df", "-h", "/").Output(); err == nil {
		lines := strings.Split(string(out), "\n")
		if len(lines) > 1 {
			fields := strings.Fields(lines[1])
			if len(fields) >= 5 {
				status = append(status, fmt.Sprintf("disk: %s used (%s)", fields[4], fields[2]))
			}
		}
	}

	// Check if claude is running in code-server
	if out, err := exec.Command("docker", "exec", "congo-dev-coder", "pgrep", "-f", "claude").Output(); err == nil && len(strings.TrimSpace(string(out))) > 0 {
		status = append(status, "agent: running")
	} else {
		status = append(status, "agent: not running")
	}

	summary := strings.Join(status, ", ")

	// Write heartbeat to DB
	models.LogEntries.Insert(&models.LogEntry{
		Type:    "heartbeat",
		Summary: summary,
		Author:  "system",
	})

	fmt.Println(summary)
}