enhance autonomy with priority scheduling, backoff retry, and status summary

This commit is contained in:
DBT
2026-02-24 01:19:49 +00:00
parent b4b2c4c221
commit 734ce19dc3
2 changed files with 144 additions and 8 deletions

View File

@@ -128,6 +128,9 @@ func statusCmd() {
fmt.Printf(" - %s\n", key)
}
}
if summary, err := collectAutonomyTaskSummary(filepath.Join(workspace, "memory", "tasks.json")); err == nil {
fmt.Printf("Autonomy Tasks: todo=%d doing=%d blocked=%d done=%d\n", summary["todo"], summary["doing"], summary["blocked"], summary["done"])
}
}
}
@@ -241,6 +244,30 @@ func collectTriggerErrorCounts(path string) (map[string]int, error) {
return counts, nil
}
func collectAutonomyTaskSummary(path string) (map[string]int, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return map[string]int{"todo": 0, "doing": 0, "blocked": 0, "done": 0}, nil
}
return nil, err
}
var items []struct {
Status string `json:"status"`
}
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
}
summary := map[string]int{"todo": 0, "doing": 0, "blocked": 0, "done": 0}
for _, it := range items {
s := strings.ToLower(strings.TrimSpace(it.Status))
if _, ok := summary[s]; ok {
summary[s]++
}
}
return summary, nil
}
func collectRecentSubagentSessions(sessionsDir string, limit int) ([]string, error) {
indexPath := filepath.Join(sessionsDir, "sessions.json")
data, err := os.ReadFile(indexPath)

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -33,6 +34,8 @@ type Options struct {
type taskState struct {
ID string
Content string
Priority string
DueAt string
Status string // idle|running|waiting|blocked|completed
LastRunAt time.Time
LastAutonomyAt time.Time
@@ -127,10 +130,12 @@ func (e *Engine) tick() {
status = "blocked"
}
}
e.state[t.ID] = &taskState{ID: t.ID, Content: t.Content, Status: status}
e.state[t.ID] = &taskState{ID: t.ID, Content: t.Content, Priority: t.Priority, DueAt: t.DueAt, Status: status}
continue
}
st.Content = t.Content
st.Priority = t.Priority
st.DueAt = t.DueAt
if st.Status == "completed" {
st.Status = "idle"
}
@@ -146,14 +151,39 @@ func (e *Engine) tick() {
}
}
dispatched := 0
ordered := make([]*taskState, 0, len(e.state))
for _, st := range e.state {
ordered = append(ordered, st)
}
sort.Slice(ordered, func(i, j int) bool {
pi := priorityWeight(ordered[i].Priority)
pj := priorityWeight(ordered[j].Priority)
if pi != pj {
return pi > pj
}
di := dueWeight(ordered[i].DueAt)
dj := dueWeight(ordered[j].DueAt)
if di != dj {
return di > dj
}
return ordered[i].ID < ordered[j].ID
})
dispatched := 0
for _, st := range ordered {
if dispatched >= e.opts.MaxDispatchPerTick {
break
}
if st.Status == "completed" || st.Status == "blocked" {
if st.Status == "completed" {
continue
}
if st.Status == "blocked" {
if now.Sub(st.LastRunAt) >= blockedRetryBackoff(st.ConsecutiveStall, e.opts.MinRunIntervalSec) {
st.Status = "idle"
} else {
continue
}
}
if !st.LastRunAt.IsZero() && now.Sub(st.LastRunAt) < time.Duration(e.opts.MinRunIntervalSec)*time.Second {
continue
}
@@ -177,8 +207,10 @@ func (e *Engine) tick() {
}
type todoItem struct {
ID string
Content string
ID string
Content string
Priority string
DueAt string
}
func (e *Engine) scanTodos() []todoItem {
@@ -202,7 +234,8 @@ func (e *Engine) scanTodos() []todoItem {
if content == "" {
continue
}
out = append(out, todoItem{ID: hashID(content), Content: content})
priority, dueAt, normalized := parseTodoAttributes(content)
out = append(out, todoItem{ID: hashID(normalized), Content: normalized, Priority: priority, DueAt: dueAt})
continue
}
if strings.HasPrefix(strings.ToLower(t), "todo:") {
@@ -210,7 +243,8 @@ func (e *Engine) scanTodos() []todoItem {
if content == "" {
continue
}
out = append(out, todoItem{ID: hashID(content), Content: content})
priority, dueAt, normalized := parseTodoAttributes(content)
out = append(out, todoItem{ID: hashID(normalized), Content: normalized, Priority: priority, DueAt: dueAt})
}
}
}
@@ -340,7 +374,8 @@ func (e *Engine) persistStateLocked() {
items = append(items, TaskItem{
ID: st.ID,
Content: st.Content,
Priority: "normal",
Priority: st.Priority,
DueAt: st.DueAt,
Status: status,
Source: "memory_todo",
UpdatedAt: nowRFC3339(),
@@ -349,6 +384,80 @@ func (e *Engine) persistStateLocked() {
_ = e.taskStore.Save(items)
}
func parseTodoAttributes(content string) (priority, dueAt, normalized string) {
priority = "normal"
normalized = strings.TrimSpace(content)
l := strings.ToLower(normalized)
if strings.HasPrefix(l, "[high]") || strings.HasPrefix(l, "p1:") {
priority = "high"
normalized = strings.TrimSpace(normalized[6:])
} else if strings.HasPrefix(l, "[low]") || strings.HasPrefix(l, "p3:") {
priority = "low"
normalized = strings.TrimSpace(normalized[5:])
} else if strings.HasPrefix(l, "[medium]") || strings.HasPrefix(l, "p2:") {
priority = "normal"
if strings.HasPrefix(l, "[medium]") {
normalized = strings.TrimSpace(normalized[8:])
} else {
normalized = strings.TrimSpace(normalized[3:])
}
}
if idx := strings.Index(strings.ToLower(normalized), " due:"); idx > 0 {
dueAt = strings.TrimSpace(normalized[idx+5:])
normalized = strings.TrimSpace(normalized[:idx])
}
if normalized == "" {
normalized = strings.TrimSpace(content)
}
return priority, dueAt, normalized
}
func priorityWeight(p string) int {
switch strings.ToLower(strings.TrimSpace(p)) {
case "high":
return 3
case "normal", "medium":
return 2
case "low":
return 1
default:
return 2
}
}
func dueWeight(dueAt string) int64 {
dueAt = strings.TrimSpace(dueAt)
if dueAt == "" {
return 0
}
layouts := []string{"2006-01-02", time.RFC3339, time.RFC3339Nano}
for _, layout := range layouts {
if t, err := time.Parse(layout, dueAt); err == nil {
return -t.Unix() // earlier due => bigger score after descending sort
}
}
return 0
}
func blockedRetryBackoff(stalls int, minRunIntervalSec int) time.Duration {
if minRunIntervalSec <= 0 {
minRunIntervalSec = 20
}
if stalls < 1 {
stalls = 1
}
base := time.Duration(minRunIntervalSec) * time.Second
factor := 1 << min(stalls, 5)
return time.Duration(factor) * base
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func hashID(s string) string {
sum := sha1.Sum([]byte(strings.ToLower(strings.TrimSpace(s))))
return hex.EncodeToString(sum[:])[:12]