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
@@ -32,7 +32,8 @@ import (
|
||||
// 并由 RejectTidyCandidatesByAction 批量拒绝(tidy_reject.go)。
|
||||
//
|
||||
// 作用域:应用只在候选自带的分区(persona_id + scope + project_id)内改行;
|
||||
// 候选与目标行的分区不一致时拒做(纵深防御 design/23 §5.2-7/8)。
|
||||
// 候选与目标行的分区不一致时拒做(纵深防御 design/23 §5.2-7/8)。受限来源
|
||||
// (群聊/陌生人)同样三类动作一律拒做(tidySkipSourceRestricted,design/23 §5.2-9/§6)。
|
||||
|
||||
// TidyApplyStatus 是一条候选应用结果的状态类别。
|
||||
type TidyApplyStatus string
|
||||
@@ -245,8 +246,13 @@ func (s *Store) applyTidyArchive(ctx context.Context, cand TidyCandidate, opts T
|
||||
return finishTidyApply(base, []string{item.ID}, err, tidySkipSourceNotActive)
|
||||
}
|
||||
|
||||
// applyTidyReclassify 修正 kind 误分类(design/23 §5.1 T0-6):只改 kind 一列,
|
||||
// applyTidyReclassify 修正 kind 误分类(design/23 §5.1 T0-6):**只改 kind 一列**,
|
||||
// 建议值来自候选证据里的 ProposedKind。
|
||||
//
|
||||
// 边界(2026-10-07 纳入自动档时写死):它不能改正文、不能改作用域、不能碰第二条行。
|
||||
// 候选上的 ProposedContent / MergedContent 之类正文字段**一律不采纳** —— 落地语句
|
||||
// 只写 kind/updated_at(reclassifyTidyItemSQL),因此即使候选是手工塞进来的畸形行,
|
||||
// 也只会得到一次分类修正,不会有任何一个字节的正文被写入。
|
||||
func (s *Store) applyTidyReclassify(ctx context.Context, cand TidyCandidate, opts TidyApplyOptions, now time.Time) (TidyApplyResult, error) {
|
||||
base := TidyApplyResult{
|
||||
CandidateID: cand.ID, Action: cand.Action, AffectedIDs: []string{},
|
||||
@@ -268,6 +274,13 @@ func (s *Store) applyTidyReclassify(ctx context.Context, cand TidyCandidate, opt
|
||||
if skip := tidyPartitionMismatch(cand, item); skip != "" {
|
||||
return skipTidyApply(base, skip), nil
|
||||
}
|
||||
if nonOwnerSource(item.SourceConversation) {
|
||||
// 只读来源(群聊/陌生人)绝不改(design/23 §5.2-9/§6):纵深防御,规则层本就
|
||||
// 把这类条目排除在扫描范围外。archive/merge 早就有这道门,reclassify 在
|
||||
// 2026-10-07 纳入自动档时必须补上同一道 —— 三类动作的只读来源口径必须一致,
|
||||
// 否则「自动档只碰主人自己的记忆」这句话就不成立。
|
||||
return skipTidyApply(base, tidySkipSourceRestricted), nil
|
||||
}
|
||||
if item.Kind == kind {
|
||||
// 已经是建议值:没有可改的,如实说「源行不在册」的口径不适用,这里
|
||||
// 用 already_decided 更像「无需再做」。
|
||||
|
||||
Reference in new issue
Block a user