From 95aba396c2ba02bb84cb166d5cc5d75b992d2a4c Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:10:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(activity):=20=E4=B8=A2=E5=BC=83=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=97=A5=E5=BF=97=E5=90=88=E5=B9=B6=E4=B8=BA=E5=8D=95?= =?UTF-8?q?=E6=9D=A1=20error,=E5=B8=A6=20reason/summary/=E7=B4=AF=E8=AE=A1?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 把「丢弃该记录」与「FK 诊断」合并成一条 error 级日志(含"丢弃"关键字, 后台只筛 error 也能看到完整信息,不再漏掉 info 级诊断行) - reason 分类:fk_violation(23503) / unique_violation(23505) / pg_error(code) / write_error - 带 summary 预览(截断去换行)+ 该任务累计丢弃条数,便于判断丢的是哪条、丢弃规模 - 新增 dropReason/bumpDrop/preview 辅助与 dropCnt 计数 --- server/engine.go | 66 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/server/engine.go b/server/engine.go index 537853e..1bcbe79 100644 --- a/server/engine.go +++ b/server/engine.go @@ -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 }