mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-17 22:54:50 +08:00
add autonomy trigger-audit logging and structured block reason persistence
This commit is contained in:
@@ -40,6 +40,7 @@ type taskState struct {
|
|||||||
Priority string
|
Priority string
|
||||||
DueAt string
|
DueAt string
|
||||||
Status string // idle|running|waiting|blocked|completed
|
Status string // idle|running|waiting|blocked|completed
|
||||||
|
BlockReason string
|
||||||
LastRunAt time.Time
|
LastRunAt time.Time
|
||||||
LastAutonomyAt time.Time
|
LastAutonomyAt time.Time
|
||||||
RetryAfter time.Time
|
RetryAfter time.Time
|
||||||
@@ -120,6 +121,18 @@ func (e *Engine) tick() {
|
|||||||
stored, _ := e.taskStore.Load()
|
stored, _ := e.taskStore.Load()
|
||||||
|
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
|
if e.hasManualPause() {
|
||||||
|
for _, st := range e.state {
|
||||||
|
if st.Status == "running" {
|
||||||
|
st.Status = "waiting"
|
||||||
|
st.BlockReason = "manual_pause"
|
||||||
|
e.writeReflectLog("waiting", st, "paused by manual switch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.persistStateLocked()
|
||||||
|
e.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
if e.hasRecentUserActivity(now) {
|
if e.hasRecentUserActivity(now) {
|
||||||
for _, st := range e.state {
|
for _, st := range e.state {
|
||||||
if st.Status == "running" {
|
if st.Status == "running" {
|
||||||
@@ -208,6 +221,7 @@ func (e *Engine) tick() {
|
|||||||
}
|
}
|
||||||
if st.Status == "waiting" {
|
if st.Status == "waiting" {
|
||||||
st.Status = "idle"
|
st.Status = "idle"
|
||||||
|
st.BlockReason = ""
|
||||||
e.writeReflectLog("resume", st, "user conversation idle, autonomy resumed")
|
e.writeReflectLog("resume", st, "user conversation idle, autonomy resumed")
|
||||||
}
|
}
|
||||||
if st.Status == "blocked" {
|
if st.Status == "blocked" {
|
||||||
@@ -216,6 +230,7 @@ func (e *Engine) tick() {
|
|||||||
}
|
}
|
||||||
if now.Sub(st.LastRunAt) >= blockedRetryBackoff(st.ConsecutiveStall, e.opts.MinRunIntervalSec) {
|
if now.Sub(st.LastRunAt) >= blockedRetryBackoff(st.ConsecutiveStall, e.opts.MinRunIntervalSec) {
|
||||||
st.Status = "idle"
|
st.Status = "idle"
|
||||||
|
st.BlockReason = ""
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -227,6 +242,7 @@ func (e *Engine) tick() {
|
|||||||
st.ConsecutiveStall++
|
st.ConsecutiveStall++
|
||||||
if st.ConsecutiveStall > e.opts.MaxConsecutiveStalls {
|
if st.ConsecutiveStall > e.opts.MaxConsecutiveStalls {
|
||||||
st.Status = "blocked"
|
st.Status = "blocked"
|
||||||
|
st.BlockReason = "max_consecutive_stalls"
|
||||||
st.RetryAfter = now.Add(blockedRetryBackoff(st.ConsecutiveStall, e.opts.MinRunIntervalSec))
|
st.RetryAfter = now.Add(blockedRetryBackoff(st.ConsecutiveStall, e.opts.MinRunIntervalSec))
|
||||||
e.sendFailureNotification(st, "max consecutive stalls reached")
|
e.sendFailureNotification(st, "max consecutive stalls reached")
|
||||||
continue
|
continue
|
||||||
@@ -235,9 +251,11 @@ func (e *Engine) tick() {
|
|||||||
|
|
||||||
e.dispatchTask(st)
|
e.dispatchTask(st)
|
||||||
st.Status = "running"
|
st.Status = "running"
|
||||||
|
st.BlockReason = ""
|
||||||
st.LastRunAt = now
|
st.LastRunAt = now
|
||||||
st.LastAutonomyAt = now
|
st.LastAutonomyAt = now
|
||||||
e.writeReflectLog("dispatch", st, "task dispatched to agent loop")
|
e.writeReflectLog("dispatch", st, "task dispatched to agent loop")
|
||||||
|
e.writeTriggerAudit("dispatch", st, "")
|
||||||
dispatched++
|
dispatched++
|
||||||
}
|
}
|
||||||
e.persistStateLocked()
|
e.persistStateLocked()
|
||||||
@@ -348,6 +366,7 @@ func (e *Engine) dispatchTask(st *taskState) {
|
|||||||
|
|
||||||
func (e *Engine) sendCompletionNotification(st *taskState) {
|
func (e *Engine) sendCompletionNotification(st *taskState) {
|
||||||
e.writeReflectLog("complete", st, "task marked completed")
|
e.writeReflectLog("complete", st, "task marked completed")
|
||||||
|
e.writeTriggerAudit("complete", st, "")
|
||||||
if !e.shouldNotify("done:" + st.ID) {
|
if !e.shouldNotify("done:" + st.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -360,6 +379,7 @@ func (e *Engine) sendCompletionNotification(st *taskState) {
|
|||||||
|
|
||||||
func (e *Engine) sendFailureNotification(st *taskState, reason string) {
|
func (e *Engine) sendFailureNotification(st *taskState, reason string) {
|
||||||
e.writeReflectLog("blocked", st, reason)
|
e.writeReflectLog("blocked", st, reason)
|
||||||
|
e.writeTriggerAudit("blocked", st, reason)
|
||||||
if !e.shouldNotify("blocked:" + st.ID) {
|
if !e.shouldNotify("blocked:" + st.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -387,6 +407,30 @@ func (e *Engine) shouldNotify(key string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) writeTriggerAudit(action string, st *taskState, errText string) {
|
||||||
|
if strings.TrimSpace(e.opts.Workspace) == "" || st == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := filepath.Join(e.opts.Workspace, "memory", "trigger-audit.jsonl")
|
||||||
|
_ = os.MkdirAll(filepath.Dir(path), 0755)
|
||||||
|
row := map[string]interface{}{
|
||||||
|
"time": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
"trigger": "autonomy",
|
||||||
|
"action": action,
|
||||||
|
"session": "autonomy:" + st.ID,
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(errText) != "" {
|
||||||
|
row["error"] = errText
|
||||||
|
}
|
||||||
|
if b, err := json.Marshal(row); err == nil {
|
||||||
|
f, oErr := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if oErr == nil {
|
||||||
|
_, _ = f.Write(append(b, '\n'))
|
||||||
|
_ = f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) writeReflectLog(stage string, st *taskState, outcome string) {
|
func (e *Engine) writeReflectLog(stage string, st *taskState, outcome string) {
|
||||||
if strings.TrimSpace(e.opts.Workspace) == "" || st == nil {
|
if strings.TrimSpace(e.opts.Workspace) == "" || st == nil {
|
||||||
return
|
return
|
||||||
@@ -457,15 +501,16 @@ func (e *Engine) persistStateLocked() {
|
|||||||
retryAfter = st.RetryAfter.UTC().Format(time.RFC3339)
|
retryAfter = st.RetryAfter.UTC().Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
items = append(items, TaskItem{
|
items = append(items, TaskItem{
|
||||||
ID: st.ID,
|
ID: st.ID,
|
||||||
Content: st.Content,
|
Content: st.Content,
|
||||||
Priority: st.Priority,
|
Priority: st.Priority,
|
||||||
DueAt: st.DueAt,
|
DueAt: st.DueAt,
|
||||||
Status: status,
|
Status: status,
|
||||||
RetryAfter: retryAfter,
|
BlockReason: st.BlockReason,
|
||||||
Source: "memory_todo",
|
RetryAfter: retryAfter,
|
||||||
DedupeHits: st.DedupeHits,
|
Source: "memory_todo",
|
||||||
UpdatedAt: nowRFC3339(),
|
DedupeHits: st.DedupeHits,
|
||||||
|
UpdatedAt: nowRFC3339(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ = e.taskStore.Save(items)
|
_ = e.taskStore.Save(items)
|
||||||
@@ -526,6 +571,22 @@ func dueWeight(dueAt string) int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) pauseFilePath() string {
|
||||||
|
if strings.TrimSpace(e.opts.Workspace) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(e.opts.Workspace, "memory", "autonomy.pause")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) hasManualPause() bool {
|
||||||
|
p := e.pauseFilePath()
|
||||||
|
if p == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := os.Stat(p)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) hasRecentUserActivity(now time.Time) bool {
|
func (e *Engine) hasRecentUserActivity(now time.Time) bool {
|
||||||
if e.opts.UserIdleResumeSec <= 0 || strings.TrimSpace(e.opts.Workspace) == "" {
|
if e.opts.UserIdleResumeSec <= 0 || strings.TrimSpace(e.opts.Workspace) == "" {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type TaskItem struct {
|
|||||||
Priority string `json:"priority"`
|
Priority string `json:"priority"`
|
||||||
DueAt string `json:"due_at,omitempty"`
|
DueAt string `json:"due_at,omitempty"`
|
||||||
Status string `json:"status"` // todo|doing|waiting|blocked|done
|
Status string `json:"status"` // todo|doing|waiting|blocked|done
|
||||||
|
BlockReason string `json:"block_reason,omitempty"`
|
||||||
RetryAfter string `json:"retry_after,omitempty"`
|
RetryAfter string `json:"retry_after,omitempty"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
DedupeHits int `json:"dedupe_hits,omitempty"`
|
DedupeHits int `json:"dedupe_hits,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user