feat(memory): 记忆整理**应用路径 + 调度修复 + 手动真跑**(TIDY-1,主人投诉的根因)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人实测: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:
AskaEth committed 2026-10-06 22:39:35 +08:00
1 parent 57d0e8c543
commit 454ddb0b70
18 files changed
+3181 -71

No files matched your search

+12 -5
View File
@@ -83,16 +83,23 @@ func TestTidyServiceNextDelay(t *testing.T) {
}
}
// TestTidyServiceStartDoesNotRunImmediately 锁定与 backthink 的**有意差异**:
// 整理不做启动即跑,进程重启不会触发一次全库扫描。
func TestTidyServiceStartDoesNotRunImmediately(t *testing.T) {
// TestTidyServiceStartOutsideQuietWindowDoesNotRunImmediately 锁定补跑判据的
// 一条边界(切片 TIDY-1):**无历史**且启动在安静窗**之外**时不做启动即跑 ——
// 新装的库不该在任意时刻烧一次全库向量化扫描,等窗口起点那一轮即可。
// 时钟是注入的固定值,因此这条断言与用例真的跑在几点无关(旧实现用
// time.Now,在 23:00–07:00 之间跑就会抖动)。
func TestTidyServiceStartOutsideQuietWindowDoesNotRunImmediately(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := newTestStore(t)
seedTidyItem(t, store, habitItem(fixtureHabitShort, tidyTestTime(t), 0.9))
svc := newTidyServiceForTest(t, store, tidyTestCfg(0.80), nil, time.Now, nil)
clock := &schedClock{at: time.Date(2026, 10, 7, 15, 0, 0, 0, time.Local)}
probe := newTidyWaitProbe(clock, 0) // 绝不真的等到窗口起点
svc := newTidyServiceForTest(t, store, tidyTestCfg(0.80), nil, clock.now, nil)
svc.wait = probe.wait
svc.Start(ctx)
probe.awaitCalls(t, 1)
if err := svc.Stop(ctx); err != nil {
t.Fatalf("Stop: %v", err)
}
@@ -101,7 +108,7 @@ func TestTidyServiceStartDoesNotRunImmediately(t *testing.T) {
t.Fatalf("ListTidyRuns: %v", err)
}
if len(runs) != 0 {
t.Fatalf("启动即跑了整理:%+v", runs)
t.Fatalf("窗口外无历史时不该启动即跑:%+v", runs)
}
}