fix(memory,loop): 记忆作用域读写成对 + source_conversation 结构化(裁决 1.9/04.4.2/04.4.3)

- 04.4.2/1.9 读侧:Retrieve 候选集并集 = 本人 persona 域 ∪ user 共享域 ∪ project 共享域(他角色 persona 域仍排除),群聊/陌生人取受限收窄
- 写侧:群聊/陌生人整段跳过记忆提取(可查不可写),persona 域仅属主可写
- 04.4.3:source_conversation 不再由 LLM 自由生成(prompt 不索要、候选结构不含该键),落库时无条件盖真实 {session_id,message_id},编解码集中在 memory/source.go
- 新增跨角色种子用例(原用例即使关掉并集也会通过)+ 成对用例「同群聊上下文:读得到共享域、写不进 store」
- 同步 store.go/retrieve.go 文档注释,与 design/03 §3/§4 口径一致
This commit is contained in:
AskaEth committed 2026-10-04 12:07:44 +08:00
1 parent af8591b78a
commit 0ce498754a
11 files changed
+428 -60

No files matched your search

+50
View File
@@ -178,6 +178,56 @@ func TestRetriever_Retrieve_EmptyPersona_正常(t *testing.T) {
}
}
func TestRetriever_Retrieve_SharedScopesUnion_共享域可读(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestStore(t)
// persona.a 自己的条目 + persona.b 写入三个作用域各一条。
addItem(t, s, MemoryItem{
PersonaID: "persona.a", Content: "a 自己的咖啡记忆", Tags: []string{"咖啡"},
Scope: ScopePersona, Kind: KindRelationship,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "用户习惯:喜欢手冲咖啡", Tags: []string{"咖啡"},
Scope: ScopeUser, Kind: KindRelationship,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "项目记忆:咖啡项目用 Go 编写", Tags: []string{"咖啡"},
Scope: ScopeProject, Kind: KindWorldEvent,
})
addItem(t, s, MemoryItem{
PersonaID: "persona.b", Content: "b 的角色日常:周末爬山", Tags: []string{"咖啡"},
Scope: ScopePersona, Kind: KindRelationship,
})
r := NewRetriever(s)
results, err := r.Retrieve(ctx, "persona.a", "咖啡", 10)
if err != nil {
t.Fatalf("Retrieve: %v", err)
}
seen := map[string]bool{}
for _, res := range results {
seen[res.Item.PersonaID+"/"+string(res.Item.Scope)] = true
}
// 可见:自己的条目 + user/project 共享域(对所有角色可读,design/03 §3/§4)。
for _, want := range []string{"persona.a/persona", "persona.b/user", "persona.b/project"} {
if !seen[want] {
t.Fatalf("candidate set missing %s (shared scopes must be unioned in): %+v", want, seen)
}
}
// 不可见:他角色的角色日常 —— 直接检索不越权,跨角色读只走
// 子代理派发(design/03 §4,design/22 §9)。
if seen["persona.b/persona"] {
t.Fatal("candidate set includes persona.b's persona-scope memory: cross-role daily memory must not be directly retrievable")
}
// 并集不得重复计入同一行。
if len(results) != 3 {
t.Fatalf("got %d results, want 3 (own + shared user + shared project, no dupes)", len(results))
}
}
func TestRetriever_Inject_Format_正常(t *testing.T) {
t.Parallel()
ctx := context.Background()