task history retention: add 3-day configurable cleanup and notify allowlist enforcement

This commit is contained in:
DBT
2026-02-28 13:10:23 +00:00
parent 6705a2b4e8
commit c7a6d34188
5 changed files with 77 additions and 0 deletions

View File

@@ -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,

View File

@@ -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": []

View File

@@ -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

View File

@@ -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: "",

View File

@@ -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) == "" {