diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index 3136553..cb1f76a 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -649,6 +649,7 @@ func buildAutonomyEngine(cfg *config.Config, msgBus *bus.MessageBus) *autonomy.E MaxConsecutiveStalls: a.MaxConsecutiveStalls, MaxDispatchPerTick: a.MaxDispatchPerTick, NotifyCooldownSec: a.NotifyCooldownSec, + QuietHours: a.QuietHours, Workspace: cfg.WorkspacePath(), DefaultNotifyChannel: a.NotifyChannel, DefaultNotifyChatID: a.NotifyChatID, diff --git a/config.example.json b/config.example.json index ca971cc..deba1bb 100644 --- a/config.example.json +++ b/config.example.json @@ -21,6 +21,7 @@ "max_consecutive_stalls": 3, "max_dispatch_per_tick": 2, "notify_cooldown_sec": 300, + "quiet_hours": "23:00-08:00", "notify_channel": "", "notify_chat_id": "" }, diff --git a/pkg/autonomy/engine.go b/pkg/autonomy/engine.go index e46a82e..cfd40bc 100644 --- a/pkg/autonomy/engine.go +++ b/pkg/autonomy/engine.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -26,6 +27,7 @@ type Options struct { DefaultNotifyChannel string DefaultNotifyChatID string NotifyCooldownSec int + QuietHours string } type taskState struct { @@ -168,6 +170,7 @@ func (e *Engine) tick() { st.Status = "running" st.LastRunAt = now st.LastAutonomyAt = now + e.writeReflectLog("dispatch", st, "task dispatched to agent loop") dispatched++ } e.persistStateLocked() @@ -231,6 +234,7 @@ func (e *Engine) dispatchTask(st *taskState) { } func (e *Engine) sendCompletionNotification(st *taskState) { + e.writeReflectLog("complete", st, "task marked completed") if !e.shouldNotify("done:" + st.ID) { return } @@ -242,6 +246,7 @@ func (e *Engine) sendCompletionNotification(st *taskState) { } func (e *Engine) sendFailureNotification(st *taskState, reason string) { + e.writeReflectLog("blocked", st, reason) if !e.shouldNotify("blocked:" + st.ID) { return } @@ -257,6 +262,9 @@ func (e *Engine) shouldNotify(key string) bool { return false } now := time.Now() + if inQuietHours(now, e.opts.QuietHours) { + return false + } if last, ok := e.lastNotify[key]; ok { if now.Sub(last) < time.Duration(e.opts.NotifyCooldownSec)*time.Second { return false @@ -266,6 +274,55 @@ func (e *Engine) shouldNotify(key string) bool { return true } +func (e *Engine) writeReflectLog(stage string, st *taskState, outcome string) { + if strings.TrimSpace(e.opts.Workspace) == "" || st == nil { + return + } + memDir := filepath.Join(e.opts.Workspace, "memory") + _ = os.MkdirAll(memDir, 0755) + path := filepath.Join(memDir, time.Now().Format("2006-01-02")+".md") + line := fmt.Sprintf("- [%s] [autonomy][%s] task=%s status=%s outcome=%s\n", time.Now().Format("15:04"), stage, st.Content, st.Status, outcome) + f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + _, _ = f.WriteString(line) +} + +func inQuietHours(now time.Time, spec string) bool { + spec = strings.TrimSpace(spec) + if spec == "" { + return false + } + parts := strings.Split(spec, "-") + if len(parts) != 2 { + return false + } + parseHM := func(v string) (int, bool) { + hm := strings.Split(strings.TrimSpace(v), ":") + if len(hm) != 2 { + return 0, false + } + h, err1 := strconv.Atoi(hm[0]) + m, err2 := strconv.Atoi(hm[1]) + if err1 != nil || err2 != nil || h < 0 || h > 23 || m < 0 || m > 59 { + return 0, false + } + return h*60 + m, true + } + start, ok1 := parseHM(parts[0]) + end, ok2 := parseHM(parts[1]) + if !ok1 || !ok2 { + return false + } + nowMin := now.Hour()*60 + now.Minute() + if start <= end { + return nowMin >= start && nowMin <= end + } + return nowMin >= start || nowMin <= end +} + func (e *Engine) persistStateLocked() { items := make([]TaskItem, 0, len(e.state)) for _, st := range e.state { diff --git a/pkg/config/config.go b/pkg/config/config.go index 295fb8d..5a9436f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -45,15 +45,16 @@ type AgentDefaults struct { } type AutonomyConfig struct { - Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ENABLED"` - TickIntervalSec int `json:"tick_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TICK_INTERVAL_SEC"` - MinRunIntervalSec int `json:"min_run_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MIN_RUN_INTERVAL_SEC"` - MaxPendingDurationSec int `json:"max_pending_duration_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_PENDING_DURATION_SEC"` - MaxConsecutiveStalls int `json:"max_consecutive_stalls" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_CONSECUTIVE_STALLS"` - MaxDispatchPerTick int `json:"max_dispatch_per_tick" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_DISPATCH_PER_TICK"` - NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"` - NotifyChannel string `json:"notify_channel" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHANNEL"` - NotifyChatID string `json:"notify_chat_id" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHAT_ID"` + Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ENABLED"` + TickIntervalSec int `json:"tick_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TICK_INTERVAL_SEC"` + MinRunIntervalSec int `json:"min_run_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MIN_RUN_INTERVAL_SEC"` + MaxPendingDurationSec int `json:"max_pending_duration_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_PENDING_DURATION_SEC"` + MaxConsecutiveStalls int `json:"max_consecutive_stalls" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_CONSECUTIVE_STALLS"` + MaxDispatchPerTick int `json:"max_dispatch_per_tick" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_DISPATCH_PER_TICK"` + NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"` + QuietHours string `json:"quiet_hours" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_QUIET_HOURS"` + NotifyChannel string `json:"notify_channel" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHANNEL"` + NotifyChatID string `json:"notify_chat_id" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHAT_ID"` } type AgentTextConfig struct { @@ -305,6 +306,7 @@ func DefaultConfig() *Config { MaxConsecutiveStalls: 3, MaxDispatchPerTick: 2, NotifyCooldownSec: 300, + QuietHours: "23:00-08:00", NotifyChannel: "", NotifyChatID: "", }, diff --git a/pkg/config/validate.go b/pkg/config/validate.go index bf8d2de..16d702f 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -102,6 +102,12 @@ func Validate(cfg *Config) []error { if aut.NotifyCooldownSec <= 0 { errs = append(errs, fmt.Errorf("agents.defaults.autonomy.notify_cooldown_sec must be > 0 when enabled=true")) } + if qh := strings.TrimSpace(aut.QuietHours); qh != "" { + parts := strings.Split(qh, "-") + if len(parts) != 2 { + errs = append(errs, fmt.Errorf("agents.defaults.autonomy.quiet_hours must be HH:MM-HH:MM")) + } + } } texts := cfg.Agents.Defaults.Texts if strings.TrimSpace(texts.NoResponseFallback) == "" {