feat(memory): 记忆整理 v1(design/23)——低峰定期扫描 + 事实包含判定 + 规则否决 + 只产建议队列(不自动应用)
This commit is contained in:
1 parent
012e9f3ad4
commit
91a3209c9b
25 files changed
+3418
-13
No files matched your search
@@ -0,0 +1,181 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 本文件是记忆整理的**候选**侧:候选与证据的落库形状、由判定结果构造候选、
|
||||
// 以及候选的幂等键。它从 tidy.go 拆出来,让编排(扫描→召回→判定→落库)
|
||||
// 保持在一屏内(AGENTS §10 文件 ≤500 行)。
|
||||
|
||||
// TidyEvidence 是一条候选的判定证据。design/23 §7 要求 diff 预览能展示
|
||||
// 「A 原文 / B 原文 / 合并后内容 / A 被 B 包含的证据 / 命中或未命中的否决规则」,
|
||||
// 因此这四项都在这里。它装的是主人自己的记忆正文,只落本机库、只走 owner-only
|
||||
// 的读侧(design/23 §11)。
|
||||
type TidyEvidence struct {
|
||||
// Similarity 是召回用的余弦相似度(只用来召回,不是判定依据)。
|
||||
Similarity float64 `json:"similarity"`
|
||||
// RecallMode 记录本轮相似度的来源(embedding / lexical),便于运维判读。
|
||||
RecallMode string `json:"recall_mode,omitempty"`
|
||||
// Containment 是包含判定结论:a_subset_b / b_subset_a / both / none。
|
||||
Containment string `json:"containment,omitempty"`
|
||||
// ContainmentEvidence 是「A ⊆ B」的逐要素证据(缺哪一项就列哪一项)。
|
||||
ContainmentEvidence []string `json:"containment_evidence,omitempty"`
|
||||
// VetoReason 是命中的否决规则(未命中时为空)。
|
||||
VetoReason string `json:"veto_reason,omitempty"`
|
||||
// KindA/KindB/ProposedKind 供 reclassify 候选展示 kind 误分类建议。
|
||||
KindA string `json:"kind_a,omitempty"`
|
||||
KindB string `json:"kind_b,omitempty"`
|
||||
ProposedKind string `json:"proposed_kind,omitempty"`
|
||||
// ContentA/ContentB 是两条原文,供面板 diff 预览。
|
||||
ContentA string `json:"content_a,omitempty"`
|
||||
ContentB string `json:"content_b,omitempty"`
|
||||
}
|
||||
|
||||
// TidyCandidate 是一条整理建议(落库形状,design/23 §10)。
|
||||
type TidyCandidate struct {
|
||||
ID string
|
||||
PersonaID string
|
||||
Scope Scope
|
||||
ProjectID string
|
||||
Kind Kind
|
||||
Action TidyAction
|
||||
SourceIDs []string
|
||||
TargetID string
|
||||
ProposedContent string
|
||||
MergedContent string
|
||||
Reason string
|
||||
Evidence TidyEvidence
|
||||
RuleVersion string
|
||||
ScoreMin float64
|
||||
ScoreMax float64
|
||||
Status string
|
||||
IdempotencyKey string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// tidyVerdict 是一条候选对的判定结果。
|
||||
type tidyVerdict struct {
|
||||
Action TidyAction
|
||||
Veto TidyVetoReason
|
||||
Target *MemoryItem
|
||||
Containment containment
|
||||
Reason string
|
||||
}
|
||||
|
||||
// archiveCandidate 为一次性事件产一条归档建议(design/23 §5.1 T0-5)。
|
||||
func archiveCandidate(it MemoryItem, now time.Time) TidyCandidate {
|
||||
return TidyCandidate{
|
||||
ID: mustNewTidyID(),
|
||||
PersonaID: it.PersonaID,
|
||||
Scope: it.Scope,
|
||||
ProjectID: it.ProjectID,
|
||||
Kind: it.Kind,
|
||||
Action: TidyArchive,
|
||||
SourceIDs: []string{it.ID},
|
||||
ProposedContent: it.Content,
|
||||
Reason: "one_off_event",
|
||||
Evidence: TidyEvidence{
|
||||
Containment: "none",
|
||||
ContentA: it.Content,
|
||||
KindA: string(it.Kind),
|
||||
},
|
||||
RuleVersion: TidyRuleVersion,
|
||||
Status: tidyCandidatePending,
|
||||
CreatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// buildCandidates 把一条判定结果转成候选:merge 需要一条存活条目(超集),
|
||||
// kind 不一致时附带一条 reclassify 建议(design/23 §5.1 T0-6)。
|
||||
func buildCandidates(pr tidyPair, v tidyVerdict, now time.Time) []TidyCandidate {
|
||||
evidence := TidyEvidence{
|
||||
Similarity: pr.score,
|
||||
RecallMode: pr.mode,
|
||||
Containment: string(v.Containment.Kind),
|
||||
ContainmentEvidence: v.Containment.Missing,
|
||||
KindA: string(pr.a.Kind),
|
||||
KindB: string(pr.b.Kind),
|
||||
ContentA: pr.a.Content,
|
||||
ContentB: pr.b.Content,
|
||||
}
|
||||
base := TidyCandidate{
|
||||
ID: mustNewTidyID(),
|
||||
PersonaID: pr.a.PersonaID,
|
||||
Scope: pr.a.Scope,
|
||||
ProjectID: pr.a.ProjectID,
|
||||
Evidence: evidence,
|
||||
RuleVersion: TidyRuleVersion,
|
||||
Status: tidyCandidatePending,
|
||||
ScoreMin: pr.score,
|
||||
ScoreMax: pr.score,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
if v.Action != TidyMerge || v.Target == nil {
|
||||
// 相似但互不包含:只建链接/分组,绝不合并(design §1.3 的 A-MEM 口径)。
|
||||
base.Action = TidyLink
|
||||
base.SourceIDs = []string{pr.a.ID, pr.b.ID}
|
||||
base.Kind = pr.a.Kind
|
||||
base.Reason = "similar_but_not_contained"
|
||||
return []TidyCandidate{base}
|
||||
}
|
||||
|
||||
absorbed, target := pr.a, *v.Target
|
||||
if v.Target.ID == pr.a.ID {
|
||||
absorbed, target = pr.b, pr.a
|
||||
}
|
||||
base.Action = TidyMerge
|
||||
base.SourceIDs = []string{absorbed.ID}
|
||||
base.TargetID = target.ID
|
||||
base.Kind = target.Kind
|
||||
base.ProposedContent = absorbed.Content
|
||||
base.MergedContent = target.Content
|
||||
base.Reason = v.Reason
|
||||
|
||||
out := []TidyCandidate{base}
|
||||
if absorbed.Kind != target.Kind {
|
||||
// kind 误分类:以存活条目(超集)的 kind 为建议值,交人工确认
|
||||
// (design/23 §5.1 T0-6「取多数票或交人工,不自动改」)。
|
||||
re := base
|
||||
re.ID = mustNewTidyID()
|
||||
re.Action = TidyReclassify
|
||||
re.SourceIDs = []string{absorbed.ID}
|
||||
re.TargetID = ""
|
||||
re.ProposedContent = ""
|
||||
re.MergedContent = ""
|
||||
re.Reason = "kind_mismatch"
|
||||
re.Evidence.ProposedKind = string(target.Kind)
|
||||
out = append(out, re)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// tidyIdempotencyKey 由 design/23 §9 的幂等键口径算出:
|
||||
// hash(persona_id, scope, project_id, sorted(source_ids), action, rule_version)。
|
||||
// 规则不变时同一批数据重复运行产出同一个键,因此候选绝不重复落库。
|
||||
func tidyIdempotencyKey(c TidyCandidate) string {
|
||||
ids := append([]string(nil), c.SourceIDs...)
|
||||
sort.Strings(ids)
|
||||
parts := []string{
|
||||
c.PersonaID, string(c.Scope), c.ProjectID,
|
||||
strings.Join(ids, ","), string(c.Action), c.RuleVersion,
|
||||
}
|
||||
sum := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// mustNewTidyID 生成候选 id。它复用与记忆条目同一个随机源;熵源损坏时回退到
|
||||
// 时间戳,而不是让整理整轮失败(候选 id 只需要在本库唯一)。
|
||||
func mustNewTidyID() string {
|
||||
id, err := newID()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("tidy-%d", time.Now().UnixNano())
|
||||
}
|
||||
return id
|
||||
}
|
||||
Reference in new issue
Block a user