fix(memory): 图整备覆盖 scenes + 证据桶按桶清(R3/R5)

R3:图整理心跳只查 entities/relations,scenes 完全没有整备路径。
Recall(nil,nil,1,"") 的全量路径只 SELECT 这两张表
(graph.go:609/631),于是同一场面的双胞胎键从建库起无人发现:
auto:chan:qq+part:morning 累积到 strength=271 / 6 features / 0 refs,
孪生的 auto:chan:qq_part:morning 持有 210 refs 却 0 features
(聚类只读 scene_features,所以它永远不被看见)。

新增 GraphDB.DedupeScenes:归一化后同名的场景合成一个——强度相加、
特征取并集(权重取大)、引用全部重定向,存活者保留 id 最小行,
跨 origin 也合。接到 mergeLoop 尾部。

为什么不塞进 detectEntityMerge 的双重循环:
- 实体是全库两两 bigram + LLM 裁决(1 万实体实测 5000 万次配对、
  ~224GB 瞬时分配每轮,是独立问题);
- 场景的判重口径是**归一化后是否同名**——同名即同一场面,键相同
  本身就是证据,不需要 LLM 裁决。而「像不像」是 EnterScene 聚类的
  职责,不是这里的事。
只做同键合并、不做相似度合并:把 chan:qq 与 chan:webui 合并是危险
的,去重不是「把像的一律合并」。

生产库副本实测(sqlite3 备份式复制到 /tmp,未碰生产):
65 个场景 → 合并 8 组 → 57 个;
auto:chan:qq_part:morning 的 refs/rel/ent 一条没丢,strength 1 → 272。

R5:createSceneLocked 新场景成立时执行的是
`DELETE FROM situation_evidence`(全表清),而证据表是多通道共用的
计数桶。后果不是「多清一点」:qq 的场景一长出来,就把 mc/webui/cli
尚未攒够 minSceneEvidence=2 的证据抹掉,它们的计数被反复清零,
于是**永远**攒不到 2 次。判据实测:6 个通道各来 3 次,只长出 2 个场景。
改为 `DELETE ... WHERE label = ?`,只清本指纹那个桶。

判据:scene_dedupe_test.go 8 例(含「不同场面不得被合并」与幂等)、
scene_evidence_test.go 3 例。均先红后绿。修 R5 时差点栽:桶键是
sig.Label(2) 本身、不带 auto: 前缀(base 才是带前缀的场景键),
第一版删错对象会「一条没删却看起来通过」,用探针实测真实桶键后改正。

记忆 8 包 + agent/core 全绿,8 包齐全、无 FAIL/panic/race。
This commit is contained in:
JianFeeeee
2026-09-26 20:16:11 +08:00
parent 8887e06274
commit 758ec11832
7 changed files with 604 additions and 5 deletions

View File

@ -410,8 +410,17 @@ func (g *GraphDB) createSceneLocked(sig Situation) (string, error) {
return "", err
}
}
// 场景成立后,把此前登记的同类线索清掉(它们已被这次长出吸收)
if _, err := tx.Exec(`DELETE FROM situation_evidence`); err != nil {
// 场景成立后,**只清本指纹那个桶**的线索(它们已被这次长出吸收)。
//
// 曾经是 `DELETE FROM situation_evidence`(全表清),后果不是「多清一点」:
// 多通道共用一个库,qq 的场景一长出来,就把 mc / webui / cli 尚未攒够
// minSceneEvidence 的证据一并抹掉——它们的计数被反复清零,于是
// **永远**攒不到 2 次,场景永远长不出来。实测:6 个通道各来 3 次,
// 只长出 2 个场景。
//
// ★ 桶键是 sig.Label(2) 本身,**不带 "auto:" 前缀**(见
// recordSituationEvidenceLocked)——base 是带前缀的场景键,两者不是一回事。
if _, err := tx.Exec(`DELETE FROM situation_evidence WHERE label = ?`, sig.Label(2)); err != nil {
return "", err
}
if err := tx.Commit(); err != nil {