diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index 00b49f4..79f95e6 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -950,6 +950,7 @@ func buildAutonomyEngine(cfg *config.Config, msgBus *bus.MessageBus) *autonomy.E QuietHours: a.QuietHours, UserIdleResumeSec: a.UserIdleResumeSec, MaxRoundsWithoutUser: a.MaxRoundsWithoutUser, + TaskHistoryRetentionDays: a.TaskHistoryRetentionDays, WaitingResumeDebounceSec: a.WaitingResumeDebounceSec, AllowedTaskKeywords: a.AllowedTaskKeywords, ImportantKeywords: cfg.Agents.Defaults.Texts.AutonomyImportantKeywords, diff --git a/config.example.json b/config.example.json index 69f6821..40444e8 100644 --- a/config.example.json +++ b/config.example.json @@ -24,7 +24,10 @@ "notify_same_reason_cooldown_sec": 900, "quiet_hours": "23:00-08:00", "user_idle_resume_sec": 20, + "max_rounds_without_user": 12, + "task_history_retention_days": 3, "waiting_resume_debounce_sec": 5, + "allowed_task_keywords": [], "notify_channel": "", "notify_chat_id": "", "notify_allow_chats": [] diff --git a/pkg/autonomy/engine.go b/pkg/autonomy/engine.go index c39e072..803ea8d 100644 --- a/pkg/autonomy/engine.go +++ b/pkg/autonomy/engine.go @@ -36,6 +36,7 @@ type Options struct { UserIdleResumeSec int WaitingResumeDebounceSec int MaxRoundsWithoutUser int + TaskHistoryRetentionDays int AllowedTaskKeywords []string ImportantKeywords []string CompletionTemplate string @@ -73,6 +74,7 @@ type Engine struct { lockOwners map[string]string roundsWithoutUser int lastDailyReportDate string + lastHistoryCleanupAt time.Time } func NewEngine(opts Options, msgBus *bus.MessageBus) *Engine { @@ -106,6 +108,9 @@ func NewEngine(opts Options, msgBus *bus.MessageBus) *Engine { if opts.MaxRoundsWithoutUser < 0 { opts.MaxRoundsWithoutUser = 0 } + if opts.TaskHistoryRetentionDays <= 0 { + opts.TaskHistoryRetentionDays = 3 + } return &Engine{ opts: opts, bus: msgBus, @@ -363,6 +368,7 @@ func (e *Engine) tick() { } e.persistStateLocked() e.maybeWriteDailyReportLocked(now) + e.maybeCleanupTaskHistoryLocked(now) } func (e *Engine) tryAcquireLocksLocked(st *taskState) bool { @@ -919,6 +925,68 @@ func appendUniqueReport(path, content, date string) error { return err } +func (e *Engine) maybeCleanupTaskHistoryLocked(now time.Time) { + if e.opts.TaskHistoryRetentionDays <= 0 { + return + } + if !e.lastHistoryCleanupAt.IsZero() && now.Sub(e.lastHistoryCleanupAt) < time.Hour { + return + } + workspace := e.opts.Workspace + if workspace == "" { + return + } + cutoff := now.AddDate(0, 0, -e.opts.TaskHistoryRetentionDays) + + // Cleanup task-audit.jsonl by event time + auditPath := filepath.Join(workspace, "memory", "task-audit.jsonl") + if b, err := os.ReadFile(auditPath); err == nil { + lines := strings.Split(string(b), "\n") + kept := make([]string, 0, len(lines)) + for _, ln := range lines { + if ln == "" { + continue + } + var row map[string]interface{} + if json.Unmarshal([]byte(ln), &row) != nil { + continue + } + ts := fmt.Sprintf("%v", row["time"]) + tm, err := time.Parse(time.RFC3339, ts) + if err != nil || tm.After(cutoff) { + kept = append(kept, ln) + } + } + _ = os.WriteFile(auditPath, []byte(strings.Join(kept, "\n")+"\n"), 0644) + } + + // Cleanup tasks.json old terminal states + tasksPath := filepath.Join(workspace, "memory", "tasks.json") + if b, err := os.ReadFile(tasksPath); err == nil { + var items []TaskItem + if json.Unmarshal(b, &items) == nil { + kept := make([]TaskItem, 0, len(items)) + for _, it := range items { + st := strings.ToLower(it.Status) + terminal := st == "done" || st == "completed" || st == "suppressed" || st == "error" + ts := strings.TrimSpace(it.UpdatedAt) + if !terminal || ts == "" { + kept = append(kept, it) + continue + } + tm, err := time.Parse(time.RFC3339, ts) + if err != nil || tm.After(cutoff) { + kept = append(kept, it) + } + } + if out, err := json.MarshalIndent(kept, "", " "); err == nil { + _ = os.WriteFile(tasksPath, out, 0644) + } + } + } + e.lastHistoryCleanupAt = now +} + func parseTodoAttributes(content string) (priority, dueAt, normalized string) { priority = "normal" normalized = content diff --git a/pkg/config/config.go b/pkg/config/config.go index 4e320fe..8275250 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -56,6 +56,7 @@ type AutonomyConfig struct { 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"` MaxRoundsWithoutUser int `json:"max_rounds_without_user" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_ROUNDS_WITHOUT_USER"` + TaskHistoryRetentionDays int `json:"task_history_retention_days" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TASK_HISTORY_RETENTION_DAYS"` 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"` @@ -327,6 +328,7 @@ func DefaultConfig() *Config { QuietHours: "23:00-08:00", UserIdleResumeSec: 20, MaxRoundsWithoutUser: 12, + TaskHistoryRetentionDays: 3, WaitingResumeDebounceSec: 5, AllowedTaskKeywords: []string{}, NotifyChannel: "", diff --git a/pkg/config/validate.go b/pkg/config/validate.go index 200a204..fd63654 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -117,6 +117,9 @@ func Validate(cfg *Config) []error { if aut.WaitingResumeDebounceSec <= 0 { errs = append(errs, fmt.Errorf("agents.defaults.autonomy.waiting_resume_debounce_sec must be > 0 when enabled=true")) } + if aut.TaskHistoryRetentionDays <= 0 { + errs = append(errs, fmt.Errorf("agents.defaults.autonomy.task_history_retention_days must be > 0 when enabled=true")) + } } texts := cfg.Agents.Defaults.Texts if strings.TrimSpace(texts.NoResponseFallback) == "" {