align heartbeat/cron flow with openclaw-style triggers

This commit is contained in:
DBT
2026-02-23 12:15:42 +00:00
parent ca64fa1559
commit 3049b84244
4 changed files with 100 additions and 11 deletions

View File

@@ -60,9 +60,47 @@ func gatewayCmd() {
msgBus := bus.NewMessageBus()
cronStorePath := filepath.Join(filepath.Dir(getConfigPath()), "cron", "jobs.json")
cronService := cron.NewCronService(cronStorePath, nil)
cronService := cron.NewCronService(cronStorePath, func(job *cron.CronJob) (string, error) {
if job == nil || strings.TrimSpace(job.Payload.Message) == "" {
return "", nil
}
targetChannel := strings.TrimSpace(job.Payload.Channel)
targetChatID := strings.TrimSpace(job.Payload.To)
if targetChannel == "" || targetChatID == "" {
targetChannel = "internal"
targetChatID = "cron"
}
msgBus.PublishInbound(bus.InboundMessage{
Channel: "system",
SenderID: "cron",
ChatID: fmt.Sprintf("%s:%s", targetChannel, targetChatID),
Content: job.Payload.Message,
SessionKey: fmt.Sprintf("cron:%s", job.ID),
Metadata: map[string]string{
"trigger": "cron",
"job_id": job.ID,
},
})
return "scheduled", nil
})
configureCronServiceRuntime(cronService, cfg)
heartbeatService := heartbeat.NewHeartbeatService(cfg.WorkspacePath(), nil, 30*60, true)
hbInterval := cfg.Agents.Defaults.Heartbeat.EverySec
if hbInterval <= 0 {
hbInterval = 30 * 60
}
heartbeatService := heartbeat.NewHeartbeatService(cfg.WorkspacePath(), func(prompt string) (string, error) {
msgBus.PublishInbound(bus.InboundMessage{
Channel: "system",
SenderID: "heartbeat",
ChatID: "internal:heartbeat",
Content: prompt,
SessionKey: "heartbeat:default",
Metadata: map[string]string{
"trigger": "heartbeat",
},
})
return "queued", nil
}, hbInterval, cfg.Agents.Defaults.Heartbeat.Enabled)
sentinelService := sentinel.NewService(
getConfigPath(),
cfg.WorkspacePath(),

View File

@@ -36,6 +36,7 @@ type AgentLoop struct {
compactionEnabled bool
compactionTrigger int
compactionKeepRecent int
heartbeatAckMaxChars int
running bool
}
@@ -131,6 +132,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
compactionEnabled: cfg.Agents.Defaults.ContextCompaction.Enabled,
compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages,
compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages,
heartbeatAckMaxChars: cfg.Agents.Defaults.Heartbeat.AckMaxChars,
running: false,
}
@@ -162,6 +164,9 @@ func (al *AgentLoop) Run(ctx context.Context) error {
}
if response != "" {
if al.shouldSuppressOutbound(msg, response) {
continue
}
al.bus.PublishOutbound(bus.OutboundMessage{
Channel: msg.Channel,
ChatID: msg.ChatID,
@@ -178,6 +183,27 @@ func (al *AgentLoop) Stop() {
al.running = false
}
func (al *AgentLoop) shouldSuppressOutbound(msg bus.InboundMessage, response string) bool {
if msg.Metadata == nil {
return false
}
trigger := strings.ToLower(strings.TrimSpace(msg.Metadata["trigger"]))
if trigger != "heartbeat" {
return false
}
r := strings.TrimSpace(response)
if !strings.HasPrefix(r, "HEARTBEAT_OK") {
return false
}
maxChars := al.heartbeatAckMaxChars
if maxChars <= 0 {
maxChars = 64
}
return len(r) <= maxChars
}
func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey string) (string, error) {
msg := bus.InboundMessage{
Channel: "cli",

View File

@@ -37,10 +37,17 @@ type AgentDefaults struct {
MaxTokens int `json:"max_tokens" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
Heartbeat HeartbeatConfig `json:"heartbeat"`
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
RuntimeControl RuntimeControlConfig `json:"runtime_control"`
}
type HeartbeatConfig struct {
Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_ENABLED"`
EverySec int `json:"every_sec" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_EVERY_SEC"`
AckMaxChars int `json:"ack_max_chars" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_ACK_MAX_CHARS"`
}
type RuntimeControlConfig struct {
IntentMaxInputChars int `json:"intent_max_input_chars" env:"CLAWGO_INTENT_MAX_INPUT_CHARS"`
AutonomyTickIntervalSec int `json:"autonomy_tick_interval_sec" env:"CLAWGO_AUTONOMY_TICK_INTERVAL_SEC"`
@@ -254,6 +261,11 @@ func DefaultConfig() *Config {
MaxTokens: 8192,
Temperature: 0.7,
MaxToolIterations: 20,
Heartbeat: HeartbeatConfig{
Enabled: true,
EverySec: 30 * 60,
AckMaxChars: 64,
},
ContextCompaction: ContextCompactionConfig{
Enabled: true,
Mode: "summary",

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"clawgo/pkg/lifecycle"
@@ -29,7 +30,7 @@ func NewHeartbeatService(workspace string, onHeartbeat func(string) (string, err
func (hs *HeartbeatService) Start() error {
if !hs.enabled {
return fmt.Errorf("heartbeat service is disabled")
return nil
}
hs.runner.Start(hs.runLoop)
return nil
@@ -73,24 +74,22 @@ func (hs *HeartbeatService) checkHeartbeat() {
}
func (hs *HeartbeatService) buildPrompt() string {
notesDir := filepath.Join(hs.workspace, "memory")
notesFile := filepath.Join(notesDir, "HEARTBEAT.md")
notesFile := filepath.Join(hs.workspace, "HEARTBEAT.md")
var notes string
if data, err := os.ReadFile(notesFile); err == nil {
notes = string(data)
candidate := string(data)
if !isEffectivelyEmptyMarkdown(candidate) {
notes = candidate
}
}
now := time.Now().Format("2006-01-02 15:04")
prompt := fmt.Sprintf(`# Heartbeat Check
prompt := fmt.Sprintf(`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
Current time: %s
Check if there are any tasks I should be aware of or actions I should take.
Review the memory file for any important updates or changes.
Be proactive in identifying potential issues or improvements.
%s
`, now, notes)
@@ -108,3 +107,17 @@ func (hs *HeartbeatService) log(message string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
f.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, message))
}
func isEffectivelyEmptyMarkdown(content string) bool {
for _, line := range strings.Split(content, "\n") {
t := strings.TrimSpace(line)
if t == "" {
continue
}
if strings.HasPrefix(t, "#") {
continue
}
return false
}
return true
}