feat(memory): reclassify 纳入自动档(主人 2026-10-07「全自动挡吧」)
自动落地现在三类:merge(字面重复 ∨ 事实等价 ∧ ≥0.95)· archive(one_off_event 且不像习惯)· **reclassify(新增)**。reclassify 只改 `kind` 一列 (`reclassifyTidyItemSQL` 只写 kind+updated_at),**不动正文、不动作用域、不碰第二条行**。 判据(纯函数 `tidyAutoReclassifyKind`,四个条件全中才自动): ① 恰 1 个 source 且无 target;② 理由 = `kind_mismatch`;③ 建议 kind 合法; ④ **改前 kind 合法且 ≠ 建议值** —— ③④ 是 **revert 抓手**:没有它自动落地就不可回滚。 **边界**:只读 ProposedKind / KindA,**一概不读** ProposedContent / MergedContent / ContentA/B / 分区 / 时间字段。 六条护栏逐条覆盖它:软动作(同一条 apply 路径)/ 每轮上限(三类同占一个池子, 默认 50 硬顶 500,混合时截断如实)/ 审计 `memory.tidy.apply`(`auto=true`, `reclassified=%d` + stats + 日志 + run 审计,**只带计数与理由、不带正文**)/ **可 revert**(`revertTidyReclassify` 按 KindA 写回,且只在当前 kind 仍等于建议值时生效)/ 幂等(候选条件更新 + 行 `AND status='active'` 双闸)/ 开关关闭 ⇒ 连查询都不发。 另补一道本片新增的门:**受限来源**(群聊/陌生人)与跨分区的 reclassify **skipped** (原先只有 merge/archive 有这道门)。 验收(主会话复跑 + 亲自变异):整树 `go build ./...` + vet 绿;memory/gateway 两包 测试绿;我用 -overlay 禁掉「必须真的变了」那一格 ⇒ `…EligibilityBoundary` **3 条子例变红** (改前 kind 缺失/非法/与建议值相同 ⇒ 都"没法回滚")。子代理另做 4 个变异(连正文一起写 / 摘受限来源门 / 去掉 reclassified 计数 / 不要求可回滚抓手)全红。 🔴 **未动的红线(一行未改)**:`archive` 的「习惯类不归档」守卫(design/23 §5.2-1) —— `looksLikeHabit` / `tidyHabitMarkers` / `tidyReasonOneOffEvent` 零改动, `TestTidyAutoNeverArchivesHabitMisjudgedAsOneOff` 仍绿。要连它也自动请主人明说。 ⚠️ 未部署(要一次 build + restart);⚠️ design/23 的 §7「reclassify 交人工」与 §13 开放点 5「全部人工应用」**现已过期**,主会话同步。 ⚠️ 已知遗留(需新授权):历史积压里指向**已被吸收行**的 reclassify 候选会每轮被选中 又永远 `source_not_active` 跳过,白占一次每轮名额;彻底解法是「永久不可用的候选自动 标 rejected」(新行为)。
This commit is contained in:
1 parent
6fe68ecfac
commit
f310173b4c
11 files changed
+757
-98
No files matched your search
@@ -10,60 +10,23 @@ import (
|
||||
)
|
||||
|
||||
// 本文件锁定记忆整理的**轮末自动落地**(2026-10-07 主人口径「那么多 link 是干什么的?
|
||||
// 该删就删,怎么这个记忆整理机制这么保守呢」;实现见 tidy_auto.go)。八条判据:
|
||||
// 该删就删,怎么这个记忆整理机制这么保守呢」+ 同日追加「全自动挡吧」;实现见
|
||||
// tidy_auto.go)。判据:
|
||||
//
|
||||
// ① 字面重复(两侧逐字相同)⇒ 自动 merge;
|
||||
// ② 事实集合等价 ∧ 相似度 ≥ 0.95 ⇒ 自动 merge;
|
||||
// ③ 低置信 ⇒ 不自动(候选在**规则层**就被否决,自动档根本看不到它);
|
||||
// ④ 一次性事件 ⇒ 自动 archive;
|
||||
// ⑤ 习惯类被钟点误判成一次性事件 ⇒ **不**自动 archive(红线守卫);
|
||||
// ⑥ 每轮上限生效、超限如实截断,下一轮继续排干;
|
||||
// ⑤ 习惯类被钟点误判成一次性事件 ⇒ **不**自动 archive(红线守卫,不许动);
|
||||
// ⑥ 每轮上限生效、超限如实截断,下一轮继续排干(三类动作同占一个池子);
|
||||
// ⑦ 开关关闭 ⇒ 回到只产建议(一条都不落地);
|
||||
// ⑧ revert 能回滚自动落地的那一条(merge 与 archive 各一次)。
|
||||
// ⑧ revert 能回滚自动落地的那一条(merge 与 archive 各一次);
|
||||
// ⑨ kind 误分类 ⇒ 自动 reclassify(实现见 TestTidyAutoReclassifyKindMismatch),
|
||||
// 只改 kind 一列:**正文一字不动**、**回滚能把 kind 复原**、**畸形候选里的
|
||||
// proposed_content 一律不采纳**(TestTidyAutoReclassifyNeverAdoptsProposedContent)。
|
||||
//
|
||||
// 外加两条「不许自动做的事」:真子集合并(非字面重复/等价)不自动;跨分区与
|
||||
// 群聊来源的候选在**执行面**被拒。
|
||||
|
||||
const (
|
||||
// fixtureHabitClock 是「被钟点误判」的典型:正文里带绝对时刻(23:30)⇒ 规则层
|
||||
// 会把它当一次性事件产 archive 候选,但它明明白白是**作息习惯**(「常在」「习惯」)。
|
||||
fixtureHabitClock = "主人常在深夜23:30后还醒着,有熬夜习惯"
|
||||
// fixtureDupText 是字面重复用例的正文(两侧逐字相同)。
|
||||
fixtureDupText = "主人喜欢在深夜喝冰美式"
|
||||
// fixtureEquivA / fixtureEquivB 是**事实集合等价但不同措辞**的一对
|
||||
// (关键要素逐一互相包含 ⇒ containBoth,正文不同 ⇒ 不是字面重复)。
|
||||
fixtureEquivA = "主人喜欢喝红牛"
|
||||
fixtureEquivB = "红牛,主人喜欢喝"
|
||||
)
|
||||
|
||||
// newAutoTestTidier 构建整理器并把审计出口捕获下来(自动档的审计是断言面之一)。
|
||||
func newAutoTestTidier(t *testing.T, store *Store, cfg TidyConfig, emb embedding.Embedder) (*Tidier, *[]tidyAuditRow) {
|
||||
t.Helper()
|
||||
rows := make([]tidyAuditRow, 0)
|
||||
tr, err := NewTidier(cfg, TidyDeps{
|
||||
Store: store,
|
||||
Embedder: emb,
|
||||
Log: testLogger(),
|
||||
Audit: func(who, what, result string) {
|
||||
rows = append(rows, tidyAuditRow{who: who, what: what, result: result})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewTidier: %v", err)
|
||||
}
|
||||
return tr, &rows
|
||||
}
|
||||
|
||||
// autoAuditRows 返回捕获到的 memory.tidy.apply(自动档)审计行。
|
||||
func autoAuditRows(rows *[]tidyAuditRow) []tidyAuditRow {
|
||||
out := make([]tidyAuditRow, 0)
|
||||
for _, r := range *rows {
|
||||
if r.what == tidyAutoApplyVerb {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
// 外加几条「不许自动做的事」:真子集合并(非字面重复/等价)不自动;跨分区与
|
||||
// 群聊/陌生人来受限来源的候选在**执行面**被拒(三类动作一视同仁,reclassify 也一样)。
|
||||
|
||||
// TestTidyAutoMergeLiteralDuplicate 是判据 ①:两侧正文逐字相同 ⇒ 候选由轮末自动档
|
||||
// 直接落地(软合并),且落地时**只动 status/merged_***,绝不改正文、绝不删行。
|
||||
@@ -467,7 +430,7 @@ func TestTidyAutoSkipsRestrictedSourceAndForeignPartition(t *testing.T) {
|
||||
ID: mustNewTidyID(), PersonaID: owner.PersonaID, Scope: ScopeUser, Kind: owner.Kind,
|
||||
Action: TidyMerge, SourceIDs: []string{owner.ID}, TargetID: group.ID,
|
||||
ProposedContent: owner.Content, MergedContent: group.Content,
|
||||
Reason: tidyReasonFactSubset,
|
||||
Reason: tidyReasonFactSubset,
|
||||
Evidence: TidyEvidence{
|
||||
Similarity: 1, Containment: string(containBoth),
|
||||
ContentA: owner.Content, ContentB: owner.Content,
|
||||
@@ -497,15 +460,3 @@ func TestTidyAutoSkipsRestrictedSourceAndForeignPartition(t *testing.T) {
|
||||
t.Fatalf("跨分区候选改动了行:%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// countArchived 数给定 id 里已归档的行数。
|
||||
func countArchived(t *testing.T, store *Store, ids []string) int {
|
||||
t.Helper()
|
||||
n := 0
|
||||
for _, id := range ids {
|
||||
if loadTidyItemRow(t, store, id).status == tidyStatusArchived {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in new issue
Block a user