feat(activity): 丢弃记录日志合并为单条 error,带 reason/summary/累计数

- 把「丢弃该记录」与「FK 诊断」合并成一条 error 级日志(含"丢弃"关键字,
  后台只筛 error 也能看到完整信息,不再漏掉 info 级诊断行)
- reason 分类:fk_violation(23503) / unique_violation(23505) / pg_error(code) / write_error
- 带 summary 预览(截断去换行)+ 该任务累计丢弃条数,便于判断丢的是哪条、丢弃规模
- 新增 dropReason/bumpDrop/preview 辅助与 dropCnt 计数
This commit is contained in:
Autumn-27 committed 2026-08-11 09:10:35 -04:00
1 parent ea833ad992
commit 95aba396c2
1 file changed
+55 -11
+55 -11
View File
@@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/Autumn-27/artex/agent"
@@ -25,6 +26,44 @@ func isFKViolation(err error) bool {
return errors.As(err, &pgErr) && pgErr.Code == "23503"
}
// dropReason classifies why an activity write was dropped, so the log can be
// grouped/analysed by cause rather than by raw error text.
func dropReason(err error) string {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "23503":
return "fk_violation(23503,父exploration不存在)"
case "23505":
return "unique_violation(23505)"
default:
return "pg_error(" + pgErr.Code + ")"
}
}
return "write_error"
}
// bumpDrop increments and returns the running count of dropped (unpersistable)
// activity records for a task. Concurrent planner + worker emits race here, so the
// counter is an atomic behind sync.Map. The count in the log shows loss scale at a
// glance instead of forcing a grep-and-count.
func (e *Engine) bumpDrop(taskID string) int64 {
v, _ := e.dropCnt.LoadOrStore(taskID, new(int64))
return atomic.AddInt64(v.(*int64), 1)
}
// preview collapses newlines and trims s to a short rune-safe snippet for one-line
// log output (avoids dumping a multi-KB summary/detail into the log).
func preview(s string, n int) string {
s = strings.ReplaceAll(s, "\r", " ")
s = strings.ReplaceAll(s, "\n", " ")
r := []rune(s)
if len(r) > n {
return string(r[:n]) + "…"
}
return string(r)
}
// model_error(provider/API 故障:LLM 层瞬时重试耗尽,或流已开始后中途断流)
// 收场的 work 不是「试过没做完」也不是真失败,而是外部抖动。默认把它当永久
// blocked 会白丢一条意图,所以这里对该终态额外重跑几次,每次之间退避一下,给
@@ -53,6 +92,7 @@ type Engine struct {
started sync.Map // taskID -> bool, so Run is idempotent per task
lastAct sync.Map // taskID -> int64 unix, last planner/worker activity (heartbeat)
paused sync.Map // taskID -> bool, user-paused (planner + workers idle but loops alive)
dropCnt sync.Map // taskID -> *int64, running count of dropped (unpersistable) activity records
// per-task execution context: each planner.Plan / worker.Execute runs under it,
// so pausing can CANCEL an in-flight run (not just skip the next one). Recreated
@@ -279,22 +319,26 @@ func (e *Engine) emitActivity(t *Task, r db.Activity) {
id, err := e.appendActivity(t, r)
if err != nil {
// NO LONGER SILENT: dropping a record breaks command↔result pairing in the
// trace — a tool_use whose tool_result was lost shows as "执行中" forever.
log.Printf("[activity] task %s 重试后仍失败、丢弃该记录 (kind=%s tool=%s tuid=%s): %v",
t.ID, r.Kind, r.Tool, r.ToolUseID, err)
// On the FK-parent failure (23503) probe the live DB so the next occurrence
// records WHY the exploration is unreachable instead of just that it is.
// trace — a tool_use whose tool_result was lost shows as "执行中" forever, and
// a lost 'result'/'round' record leaves the session with no summary ("无总结").
// Everything needed to分析根因 goes into ONE error-level line: reason class,
// summary preview, running drop count for this task, and — on the FK case — a
// live probe of WHY the parent exploration is unreachable.
n := e.bumpDrop(t.ID)
diag := ""
// On the FK-parent failure (23503) probe the live DB so the log records WHY the
// exploration is unreachable (row gone / wrong expID) instead of just that it is.
if isFKViolation(err) {
storeID := t.Store.ID()
exists, refs, maxID, dErr := e.m.pg.ExplorationDiag(storeID)
if dErr != nil {
log.Printf("[activity] task %s FK 诊断查询失败 (store.expID=%d task.ExpID=%d): %v",
t.ID, storeID, t.ExpID, dErr)
if exists, refs, maxID, dErr := e.m.pg.ExplorationDiag(storeID); dErr != nil {
diag = fmt.Sprintf(" | FK诊断查询失败(store.expID=%d task.ExpID=%d): %v", storeID, t.ExpID, dErr)
} else {
log.Printf("[activity] task %s FK 诊断: store.expID=%d task.ExpID=%d exploration存在=%v 引用它的task数=%d 当前MAX(exploration.id)=%d",
t.ID, storeID, t.ExpID, exists, refs, maxID)
diag = fmt.Sprintf(" | FK诊断: store.expID=%d task.ExpID=%d exploration存在=%v 引用它的task数=%d MAX(exploration.id)=%d",
storeID, t.ExpID, exists, refs, maxID)
}
}
log.Printf("[activity] task %s 丢弃活动记录(该任务累计第 %d 条) worker=%s kind=%s tool=%s tuid=%s reason=%s summary=%q: %v%s",
t.ID, n, r.Worker, r.Kind, r.Tool, r.ToolUseID, dropReason(err), preview(r.Summary, 80), err, diag)
e.touch(t.ID)
return
}