mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 21:03:16 +00:00
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。
130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
package memory
|
||
|
||
// R5 的判据:场景成立时清证据,只清**本指纹那个桶**,不是全表。
|
||
//
|
||
// 现状(scene_emerge.go createSceneLocked 末尾):
|
||
//
|
||
// DELETE FROM situation_evidence ← 全表清
|
||
//
|
||
// 后果:多场景并发轮次下,A 场景的建立会连带清掉 B 尚未攒够
|
||
// minSceneEvidence=2 的证据 ⇒ 门槛判定被「别的场景刚好长出来」随机打断。
|
||
// 这不是理论:QQ / mc / webui 三个通道在同一进程里各自计数。
|
||
//
|
||
// 判据参照物在生产代码之外:期望值是「建一个场景后,其它桶的证据必须原样还在」。
|
||
|
||
import (
|
||
"os"
|
||
"testing"
|
||
)
|
||
|
||
// evidenceCount 读某桶的累计次数(直查表,不走被测函数)。
|
||
func evidenceCount(t *testing.T, g *GraphDB, label string) int {
|
||
t.Helper()
|
||
var n int
|
||
err := g.db.QueryRow(
|
||
`SELECT COALESCE((SELECT count FROM situation_evidence WHERE label = ?), 0)`, label,
|
||
).Scan(&n)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return n
|
||
}
|
||
|
||
// A 场景成立时,B 桶的证据不得被动。
|
||
func TestSceneCreationKeepsOtherEvidence(t *testing.T) {
|
||
g := newTestGraph(t)
|
||
defer os.Remove(g.dbPath)
|
||
defer g.Close()
|
||
|
||
qq := mkSig("qq", "", "")
|
||
webui := mkSig("webui", "", "")
|
||
|
||
// 两个通道各登记一次证据(都还没到门槛)
|
||
if _, _, err := g.EnterScene(qq); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, _, err := g.EnterScene(webui); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 桶键 = sig.Label(2) 本身,**不带 "auto:" 前缀**(已用探针实测:
|
||
// 登记出来的 label 是 "chan:qq" / "chan:webui")。
|
||
if evidenceCount(t, g, NormalizeSceneKey("chan:webui")) == 0 {
|
||
t.Fatal("webui 桶的证据没登记上,前提不成立")
|
||
}
|
||
|
||
// qq 第 2 次 → 建出场景
|
||
key, _, err := g.EnterScene(qq)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if key == "" {
|
||
t.Fatal("qq 第 2 次应建出场景")
|
||
}
|
||
|
||
// ★ 关键断言:qq 的证据被吸收了,webui 的必须还在
|
||
if n := evidenceCount(t, g, NormalizeSceneKey("chan:webui")); n != 1 {
|
||
t.Errorf("建 qq 场景时把 webui 桶的证据清了(剩 %d,应为 1)—— "+
|
||
"DELETE FROM situation_evidence 是全表清,"+
|
||
"别的场景的门槛计数被这次建键随机打断了", n)
|
||
}
|
||
}
|
||
|
||
// 被吸收的应该是**本指纹**那个桶。
|
||
func TestSceneCreationClearsOwnEvidence(t *testing.T) {
|
||
g := newTestGraph(t)
|
||
defer os.Remove(g.dbPath)
|
||
defer g.Close()
|
||
|
||
qq := mkSig("qq", "", "")
|
||
webui := mkSig("webui", "", "")
|
||
|
||
g.EnterScene(qq)
|
||
g.EnterScene(webui)
|
||
qqLabel := NormalizeSceneKey(qq.Label(2)) // 桶键不带 auto: 前缀
|
||
if evidenceCount(t, g, qqLabel) == 0 {
|
||
t.Fatalf("前提不成立:%q 桶应有证据", qqLabel)
|
||
}
|
||
|
||
if _, _, err := g.EnterScene(qq); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if n := evidenceCount(t, g, qqLabel); n != 0 {
|
||
t.Errorf("场景已成立,%q 桶的证据应被吸收(该场面已长出场景),实际仍为 %d", qqLabel, n)
|
||
}
|
||
}
|
||
|
||
// 反复建多个场景,早期桶的证据必须能活到自己的门槛。
|
||
// 这是 R5 的真实后果形态:三个通道轮流入,每个都只来过一次,
|
||
// 全表清会让它们**永远**攒不到 2 次。
|
||
func TestEvidenceSurvivesOtherSceneCreations(t *testing.T) {
|
||
g := newTestGraph(t)
|
||
defer os.Remove(g.dbPath)
|
||
defer g.Close()
|
||
|
||
sigs := map[string]Situation{
|
||
"qq": mkSig("qq", "", ""),
|
||
"mc": mkSig("mc", "", ""),
|
||
"cli": mkSig("cli", "", ""),
|
||
"acp": mkSig("acp", "", ""),
|
||
"http": mkSig("http", "", ""),
|
||
"timer": mkSig("timer", "", ""),
|
||
}
|
||
|
||
// 每个通道轮流来 3 次。若全表清,后到的通道永远攒不够。
|
||
for round := 0; round < 3; round++ {
|
||
for _, sig := range sigs {
|
||
if _, _, err := g.EnterScene(sig); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6 个通道都该长出场景
|
||
if n := len(sceneKeys(t, g)); n != len(sigs) {
|
||
t.Errorf("6 个通道各来 3 次,应长出 %d 个场景,实际 %d: %v\n"+
|
||
"全表清证据会让除第一个之外的通道永远攒不到 minSceneEvidence",
|
||
len(sigs), n, sceneKeys(t, g))
|
||
}
|
||
}
|