feat(memory): 记忆整理**应用路径 + 调度修复 + 手动真跑**(TIDY-1,主人投诉的根因)
主人实测:memory_items 1191 行、候选 77 条全 pending、memory_tidy_runs 仅 1 行、两晚定时没跑。 根因两条(都在这片里修掉): ① **以前根本没有执行面** —— 只有只读试算,候选永远 pending ② **调度被重启饿死** —— interval=24h 的 ticker 从进程启动算起,两天重启十几次 ⇒ 24h 永远走不完 - 应用路径:逐条 / 按 run 批量(默认 100、硬顶 500、超限 truncated)/ revert;**软合并** (status + merged_into/merged_from/merge_count,**绝不改 content、行不删**)+ 审计 (memory.tidy.apply / memory.tidy.revert)+ **幂等**(候选状态条件更新 + 行 status 闸两道) - 调度:改成**到点即跑**(catchUp:无历史+窗口内 / 距上次 run ≥ interval / 窗口起点晚于上次 run), 「上次 run」以 memory_tidy_runs 为**持久事实**;注入时钟用例证明**重启不饿死** - 手动真跑端点 POST /admin/memories/tidy/run(同档鉴权 + 审计 + 回执:新增/合并/软删/耗时) - 候选表加 run_id 列 + 索引(前滚;顺序=建表→补列→建索引,否则既有库会 no such column) - 3 个变异自检(删 catchUp / 去掉两道 status 闸 / truncated 恒 false) ⚠️ 生产库零动作:1191 行真跑与 77 条候选应用**未执行**(按红线由主人确认后主会话执行) ⚠️ 契约段在报告里(candidates 列表/apply/revert/tidy.run),Docs/ 待登记
This commit is contained in:
1 parent
57d0e8c543
commit
454ddb0b70
18 files changed
+3181
-71
No files matched your search
@@ -0,0 +1,100 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
)
|
||||
|
||||
// 本文件是记忆整理**手动真跑**入口(切片 TIDY-1):
|
||||
// POST /api/v1/admin/memories/tidy/run —— 立刻按同一套规则跑一轮,写候选 + 写运行
|
||||
// 留痕,并把「新增/合并/软删各多少 + 耗时」如实回执。审计由领域侧 TidyService 记
|
||||
// `memory.tidy.run`(一次点击一条,本文件不重复)。
|
||||
|
||||
// adminMemoryTidyRun 处理 POST /api/v1/admin/memories/tidy/run:手动**真跑**一轮
|
||||
// (与心跳、试算走同一套规则与配置;写候选 + 运行留痕)。它是「panic button」:
|
||||
// 主人不必等到 23:00 就能让整理真的产出一批待定候选,再决定应用。
|
||||
//
|
||||
// 审计不在这里写 —— 领域侧 TidyService 已按 design §8 记 `memory.tidy.run`
|
||||
// (result 里带 trigger=manual / run_id / 计数)。一次点击一条审计。
|
||||
func (s *Server) adminMemoryTidyRun(w http.ResponseWriter, r *http.Request) {
|
||||
if s.memoryTidy == nil {
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, tidyUnavailableCode, "记忆整理服务未接线"))
|
||||
return
|
||||
}
|
||||
runner, ok := s.memoryTidy.(MemoryTidyRunner)
|
||||
if !ok {
|
||||
writeError(w, &apperr.AppError{
|
||||
Code: "tidy_run_unavailable",
|
||||
Message: "记忆整理真跑入口未接线(只有只读试算可用)",
|
||||
HTTPCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), tidyRunTimeout)
|
||||
defer cancel()
|
||||
|
||||
stats, err := runner.ManualRun(ctx)
|
||||
if err != nil {
|
||||
writeError(w, tidyRunAppError(err))
|
||||
return
|
||||
}
|
||||
s.log.Info("memory tidy manual run finished",
|
||||
"run_id", stats.RunID, "scanned", stats.Scanned, "candidates", stats.Candidates,
|
||||
"duplicates", stats.Duplicates, "duration_ms", stats.DurationMS)
|
||||
writeJSON(w, http.StatusOK, adminTidyRunResponse(stats))
|
||||
}
|
||||
|
||||
// tidyRunAppError 把「真跑」失败映射成诚实的错误码。映射表与试算那张
|
||||
// (tidyDryRunAppError)**分开**:试算的 500/409 文案讲的是「没产出候选」,
|
||||
// 真跑讲的是「没跑成」—— 同一类别、不同事实,混用会让排障读到错的那句。
|
||||
func tidyRunAppError(err error) *apperr.AppError {
|
||||
switch {
|
||||
case errors.Is(err, memory.ErrTidyDisabled):
|
||||
return &apperr.AppError{
|
||||
Code: "tidy_disabled",
|
||||
Message: "记忆整理已被配置关闭(MEMORY_TIDY_ENABLED=false)",
|
||||
HTTPCode: http.StatusConflict,
|
||||
}
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return &apperr.AppError{
|
||||
Code: "tidy_timeout",
|
||||
Message: "记忆整理真跑超时(本轮未完整跑完,已跑部分仍写入了留痕)",
|
||||
HTTPCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
default:
|
||||
return apperr.Wrap(apperr.ErrInternal, "tidy_run_failed", "记忆整理真跑失败")
|
||||
}
|
||||
}
|
||||
|
||||
// adminTidyRunResponse 投影真跑回执。
|
||||
func adminTidyRunResponse(stats memory.TidyStats) AdminTidyRunResponse {
|
||||
byAction := stats.ByAction
|
||||
if byAction == nil {
|
||||
byAction = map[string]int{}
|
||||
}
|
||||
reasons := stats.VetoReasons
|
||||
if reasons == nil {
|
||||
reasons = map[string]int{}
|
||||
}
|
||||
return AdminTidyRunResponse{
|
||||
RunID: stats.RunID,
|
||||
Trigger: "manual",
|
||||
RuleVersion: memory.TidyRuleVersion,
|
||||
DurationMs: stats.DurationMS,
|
||||
Partitions: stats.Partitions,
|
||||
Scanned: stats.Scanned,
|
||||
Compared: stats.Compared,
|
||||
Pairs: stats.Pairs,
|
||||
Vetoed: stats.Vetoed,
|
||||
Errors: stats.Errors,
|
||||
Truncated: stats.Truncated,
|
||||
VetoReasons: reasons,
|
||||
CandidatesCreated: stats.Candidates,
|
||||
CandidatesDuplicate: stats.Duplicates,
|
||||
CandidatesByAction: byAction,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user