Files
AskaEth 54e32304f2 fix(memory): dedupe_group 补取值门 + 临时层双击复核;闸补 3 类长期事实安全阀(M34 r1 复审)
r1 复审 p1-1(会毁数据):tag_set 簇的安全门只有数值与否定,**专有名词/引号取值不比**,
生产语料实测把 `用户的昵称是「叶酱」。` 并进 `主人的昵称是「喵喵七号」…` 的存活行,
而同库另有 4 行写着「喵喵七号已作废、应称叶酱」⇒ 一次 apply 丢掉当前事实。
修法(结构性,不是补丁):
- 新增取值门 tidy_dedupe_values.go:被吸收行的**引号取值**(「」『』“”‘’")与
  **拉丁 token**(≥2 字母,含 h3R3 这类混排)必须在存活行正文中一个不少;
  方向单向(被吸收 ⊆ 存活),与软合并语义同向(存活行多出的取值不会丢)。
  补齐 design/23 §5.1-3 要求的「专有名词 / 数值 / 否定词」三件套里缺的第一件。
- tag_set 簇也过相似度下限(dedupeGroupMinSimilarity 两档统一)。取值 0.10 而非更高:
  语料实测 tag_set 合法改写(`用户可能需要提醒吃早饭…` 0.047、`叶酱有工作…` 0.109)
  的余弦本来就在 0.05~0.4 —— 抬高它只会删掉合法去重,不会多挡住危险对。
  实测(503 对):0.10→保留 98%、0.20→86%、0.30→63%、0.40→40%、0.50→20%。
- 回归用例:TestDedupeGroupRejectsSameTagDifferentValue(脱敏真实语料 + 同值对照组)、
  TestDedupeGroupLatinTokenIsAlsoAValue、TestDedupeGroupNeverLosesAValue(簇级不变式)、
  TestDedupeGroupTagSetAlsoNeedsSimilarityFloor。去掉取值门 ⇒ 前三条红;把 tag 相似度
  归零 ⇒ 第四条红。

r1 复审 p2-1(数据安全):元信息查询失败时**该分区本轮不分簇**(旧实现降级成 nil map,
`temporary` 恒 false ⇒ 绕开「临时行绝不进簇」红线);apply 侧补 `expires_at` 复核
(候选久置/生成后变临时 ⇒ 整簇 skip,tidySkipTemporaryRow)。用例:
TestDedupeInPartitionSkipsWhenMetaUnavailable、TestTidyApplyDedupeGroupSkipsTemporaryRows。

r1 复审 p2-2(闸误杀被低报):补三类安全阀 ——「以后/从此/往后/今后」= 自此刻起永久成立、
「换成/变成/改成/搬进」= 状态已改变且新状态持续、N7 的进行中标记同样必须句首 12 字内;
「宿舍」进长期事实词表;顺手修 `<=` 的 off-by-one(改为 `<`)。

实测(生产库只读 mode=ro,active 3667):
- dedupe:281 簇/594 行 → **255 簇/505 行(13.8%)**;tag_set 240 / prefix12 15;
  取值门挡 76 对、相似度门挡 12 对;簇大小 2:155 3:48 4:26 5:8 6:7 7:4 8:2 9:2 18:1 20:2
  (两个 20 行簇与 18 行簇的取值集合均为单一「喵喵七号」⇒ 大簇=同一事实的重写)。
- 闸:改前 2 (0.05%) → 改后 **75 (2.05%)**(n6 53 / n7 20 / n1 2);含锚点词的 27 条
  逐条人读均为「约定明天中午见 / 下周留个午后」类到期即失效的安排。
- 达标代价:+目前/现在 当锚点 → 2.13%;再叠一次性事件动词表(曾/发来/分享…)→ 6.74%
  且新增 172 条里 56 条是长期身份/里程碑(「i love you 文件」17 条、「海报就是助手本人」
  10 条…)⇒ 仍到不了 8%,不许为凑区间放宽。

验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
2026-10-25 00:29:02 +08:00

64 lines
2.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package memory
import (
"context"
"fmt"
"strings"
)
// 本文件是 `dedupe_group` 的**元信息查询**:按分区取出「合并次数」与「是否临时层」
// 两列 —— 它们是存活规则的次要判据与一条数据安全护栏(临时行绝不进簇)。
// 从 tidy_dedupe.go 拆出来只为守 AGENTS §10 的文件行数上限(≤500 行),
// 注释与判据**逐字未动**。
// dedupeItemMetaSQL 取一个分区内每条记忆的**分簇所需元信息**:合并次数
// (存活规则的次要判据)+ 是否是临时层(`expires_at` 非空)。
//
// 两列都只读、只走分区条件,与 tidy 分区扫描同一组谓词(不含 archived/merged)。
const dedupeItemMetaSQL = `SELECT id, merge_count, expires_at FROM memory_items
WHERE persona_id = ? AND scope = ? AND project_id = ? AND status = ?`
// tidyDedupeMeta 是一条记忆的额外元信息(`MemoryItem` 的读侧 SELECT 不含这两列)。
type tidyDedupeMeta struct {
// mergeCount 是存活规则的次要判据(合并次数多的留)。
mergeCount int
// temporary 报告这条记忆在**临时层**(`expires_at` 非空,design/03 §9.6)。
//
// 🔴 临时层行**绝不进簇**,两个方向都不许:让一条长期事实并进一个 7 天后会被
// 后台**物理删除**(design/23 §19)的存活行,等于把那条事实一起删掉 ——
// 被吸收行会永远停在 `status='merged'` 且 `merged_into` 指向一个不存在的 id。
// 临时行本身也不值得合并:它的寿命本来就只剩几天。
temporary bool
}
// dedupeItemMeta 读出一个分区的 id → 元信息映射。
//
// 🔴 **失败方向是「这个分区这轮不分簇」**(不是「降级继续」):`temporary` 承载的是
// 「临时行绝不进簇」这条数据安全红线,而 nil map 会让 `temporary` 恒为 false ——
// 那等于把红线绕开(r1 复审 P2-1 实测的正是这个口子)。merge_count 缺了确实只影响
// 「等长时留哪条」,但两列同源同一次查询,为一条次要判据去承担一条红线风险不划算。
func (s *Store) dedupeItemMeta(ctx context.Context, p tidyPartition) (map[string]tidyDedupeMeta, error) {
rows, err := s.db.QueryContext(ctx, dedupeItemMetaSQL,
p.personaID, string(p.scope), p.projectID, string(tidyStatusActive))
if err != nil {
return nil, fmt.Errorf("memory/tidy: dedupe item meta: %w", err)
}
defer func() { _ = rows.Close() }()
out := make(map[string]tidyDedupeMeta, 64)
for rows.Next() {
var (
id string
mergeCnt int
expiresAt string
)
if err := rows.Scan(&id, &mergeCnt, &expiresAt); err != nil {
return nil, fmt.Errorf("memory/tidy: scan dedupe item meta: %w", err)
}
out[id] = tidyDedupeMeta{mergeCount: mergeCnt, temporary: strings.TrimSpace(expiresAt) != ""}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("memory/tidy: dedupe item meta rows: %w", err)
}
return out, nil
}