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

+60
View File
@@ -0,0 +1,60 @@
package memory
import (
"encoding/json"
"strings"
)
// SourceRef 是 source_conversation 落库值的结构化形状(裁决
// 04.4.3,design/03 §2 溯源):一条记忆由哪次会话的哪条消息
// 提取而来。两个 id 都由持久化层(提取调用点)从运行上下文
// 盖真实值,绝不由模型生成,因此「可回溯到哪次对话」是
// 机器可验证的,而不是 LLM 自由文本。
type SourceRef struct {
// SessionID 是提取发生时的会话 id(Run 的 sessionID:Web
// 主/次级会话或渠道会话)。
SessionID string `json:"session_id"`
// MessageID 是触发提取的消息标识:gateway 在 Run 前挂载的
// turn id;调用方未挂载时为空,此时溯源只到会话粒度。
MessageID string `json:"message_id"`
}
// FormatSourceConversation 把真实 session/message id 编码为
// source_conversation 的落库值:固定键序的紧凑 JSON,可被
// ParseSourceConversation 解回。两个 id 都为空时返回 ""(无
// 溯源)。纯字符串字段结构的 Marshal 不会失败;防御性兜底
// 返回 ""。
func FormatSourceConversation(sessionID, messageID string) string {
ref := SourceRef{
SessionID: strings.TrimSpace(sessionID),
MessageID: strings.TrimSpace(messageID),
}
if ref.SessionID == "" && ref.MessageID == "" {
return ""
}
b, err := json.Marshal(ref)
if err != nil {
return ""
}
return string(b)
}
// ParseSourceConversation 解码落库的 source_conversation。ok 为
// false 表示该值不是结构化溯源 —— 裁决前由 LLM 自由文本填写
// 的遗留行会落在这里 —— 调用方据此区分可机器回溯的行与
// 遗留行,而不是把自由文本误当真实 id。解出的两个字段都为
// 空同样返回 false:没有真实 id 的行不构成溯源。
func ParseSourceConversation(s string) (SourceRef, bool) {
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "{") {
return SourceRef{}, false
}
var ref SourceRef
if err := json.Unmarshal([]byte(s), &ref); err != nil {
return SourceRef{}, false
}
if ref.SessionID == "" && ref.MessageID == "" {
return SourceRef{}, false
}
return ref, true
}