diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index b011aa4..9208979 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -949,6 +949,7 @@ func buildAutonomyEngine(cfg *config.Config, msgBus *bus.MessageBus) *autonomy.E NotifySameReasonCooldownSec: a.NotifySameReasonCooldownSec, QuietHours: a.QuietHours, UserIdleResumeSec: a.UserIdleResumeSec, + MaxRoundsWithoutUser: a.MaxRoundsWithoutUser, WaitingResumeDebounceSec: a.WaitingResumeDebounceSec, AllowedTaskKeywords: a.AllowedTaskKeywords, ImportantKeywords: cfg.Agents.Defaults.Texts.AutonomyImportantKeywords, diff --git a/pkg/autonomy/engine.go b/pkg/autonomy/engine.go index b434f4f..8426ed5 100644 --- a/pkg/autonomy/engine.go +++ b/pkg/autonomy/engine.go @@ -34,6 +34,7 @@ type Options struct { QuietHours string UserIdleResumeSec int WaitingResumeDebounceSec int + MaxRoundsWithoutUser int AllowedTaskKeywords []string ImportantKeywords []string CompletionTemplate string @@ -69,6 +70,7 @@ type Engine struct { state map[string]*taskState lastNotify map[string]time.Time lockOwners map[string]string + roundsWithoutUser int } func NewEngine(opts Options, msgBus *bus.MessageBus) *Engine { @@ -99,6 +101,9 @@ func NewEngine(opts Options, msgBus *bus.MessageBus) *Engine { if opts.WaitingResumeDebounceSec <= 0 { opts.WaitingResumeDebounceSec = 5 } + if opts.MaxRoundsWithoutUser < 0 { + opts.MaxRoundsWithoutUser = 0 + } return &Engine{ opts: opts, bus: msgBus, @@ -153,6 +158,7 @@ func (e *Engine) tick() { e.writeTriggerAudit("waiting", st, "manual_pause") } } + e.roundsWithoutUser = 0 e.persistStateLocked() e.mu.Unlock() return @@ -306,6 +312,14 @@ func (e *Engine) tick() { } } + if e.opts.MaxRoundsWithoutUser > 0 && e.roundsWithoutUser >= e.opts.MaxRoundsWithoutUser { + st.Status = "waiting" + st.BlockReason = "idle_round_budget" + st.WaitingSince = now + e.writeReflectLog("waiting", st, fmt.Sprintf("paused by idle round budget (%d)", e.opts.MaxRoundsWithoutUser)) + e.writeTriggerAudit("waiting", st, "idle_round_budget") + continue + } if !e.tryAcquireLocksLocked(st) { st.Status = "waiting" st.BlockReason = "resource_lock" @@ -325,6 +339,7 @@ func (e *Engine) tick() { st.LastAutonomyAt = now e.writeReflectLog("dispatch", st, "task dispatched to agent loop") e.writeTriggerAudit("dispatch", st, "") + e.roundsWithoutUser++ dispatched++ } e.persistStateLocked() diff --git a/pkg/config/config.go b/pkg/config/config.go index cabb9c8..71ddd04 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -54,9 +54,10 @@ type AutonomyConfig struct { NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"` NotifySameReasonCooldownSec int `json:"notify_same_reason_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_SAME_REASON_COOLDOWN_SEC"` QuietHours string `json:"quiet_hours" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_QUIET_HOURS"` - UserIdleResumeSec int `json:"user_idle_resume_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_USER_IDLE_RESUME_SEC"` - WaitingResumeDebounceSec int `json:"waiting_resume_debounce_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_WAITING_RESUME_DEBOUNCE_SEC"` - AllowedTaskKeywords []string `json:"allowed_task_keywords" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ALLOWED_TASK_KEYWORDS"` + UserIdleResumeSec int `json:"user_idle_resume_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_USER_IDLE_RESUME_SEC"` + MaxRoundsWithoutUser int `json:"max_rounds_without_user" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_ROUNDS_WITHOUT_USER"` + WaitingResumeDebounceSec int `json:"waiting_resume_debounce_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_WAITING_RESUME_DEBOUNCE_SEC"` + AllowedTaskKeywords []string `json:"allowed_task_keywords" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ALLOWED_TASK_KEYWORDS"` 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"` } @@ -324,7 +325,9 @@ func DefaultConfig() *Config { NotifySameReasonCooldownSec: 900, QuietHours: "23:00-08:00", UserIdleResumeSec: 20, + MaxRoundsWithoutUser: 12, WaitingResumeDebounceSec: 5, + AllowedTaskKeywords: []string{}, NotifyChannel: "", NotifyChatID: "", },