refactor(memory): M45 r1 复审返工 —— temporal 位只放开 N6 位置判据

按 r1 复审裁定(p1-3items / fix-then-merge)重做三处:

P1-1/P1-2:删掉自造的六条语义安全阀(temporalSanityVeto)与约定词表
(retentionStatesAgreement)。理由被复审用两条生产行否证:它们既把该救的行
杀了(「…睡得比平时早」被习惯词、「…以后不用再惦记」被永久化标记),又挡不住
同义异形的约定(说定 / 约了)。现在 `one_off` 的全部作用是**放开 N6 的位置判据**:
judgeExtractionRetention 新增 temporal 入参 → judgeRetentionTransient →
judgeRetentionTimepointWide(位置内命中仍返 n6_timepoint,只有靠放开位置才命中
才返新增的 n6_timepoint_wide,供观测区分)。N1~N5、N7、护栏①②、N6 的 1~7 条
语义安全阀一字不动;durable / recurring 仍直接判 long(fail-safe 方向)。

P1-3:上界口径改为「时点词存在 ∧ 语义安全阀全过 ∧ 当前 long」——不依赖任何
标注,可逐条列出。实测(生产库只读副本,快照 2026-10-26):最近 500 条 active
能翻 13 条、**逐条都是「约定 + 今天没走成」**;全量 3694 条能翻 26 条(13 条
约定 + 13 条状态行)。旧的「句首时点词」口径是同义反复,已删。

已知代价(写进 temporal.go 头注与 TestTemporalOneOffWidenedPositionTouchesAgreements,
不藏在词表后面):位置窗口今天正是那批约定行的唯一保护,放开后只要模型标
one_off 它们就会判 temp;而复审点名的两条目标行本片救不了(被语义阀挡,与位置
无关)。因此这一位在当前语料上**没有可测收益**,是否保留是产品裁决。

P2-1:store.go 503 → 497 行,拆出 store_temporal.go(哨兵错误 + 字段校验 +
列值编码)。
P2-2:temporal.go 头注改准 —— `''` 是「0027 之前的历史行 + 没做时间性判断的
写入路径(ManualExtractor / 网关手写 / 整理回填,它们由调用方显式给档位)」。
P2-3:报告补语境(retention_tier 非空仅 26 行、temp 1 行、temporal 列当时还不
存在 ⇒ 重放结论对新写入没有预测力)。

测试:验收点 ①~⑤ 按新语义重写;新增「位置放开可打红」的变异靶子与「N7 位置
不放宽」「无时点词时这一位不做事」「两条目标行救不了」「约定行会翻(已知风险)」
五条边界断言。变异实测:unlocksTimepointPosition 改恒 false ⇒ 三条用例红。
回归:go build ./... + go vet ./... 全绿;memory/db 包测试与 memory -race 全绿。
This commit is contained in:
AskaEth committed 2026-10-26 19:15:08 +08:00
1 parent 91101dedfb
commit 3b9ae3f918
9 files changed
+412 -370

No files matched your search

+11 -9
View File
@@ -95,12 +95,12 @@ type retentionVerdict struct {
// 2. **显式给了有效期**(`item.ExpiresAt` 非零)⇒ 这一行会到期 ⇒ 它就是临时记忆。
// 两者是同一根轴的两种表达,因此这里不是「推导」,而是同一个事实的补全。
// 3. **受限来源**(群聊 / 陌生人)⇒ 不分型(空串)。见文件头注 ②。
// 4. **提取器的时间性元数据位**(迁移 0027、temporal.go)⇒ `one_off` 判 temp
// (先过 temporalSanityVeto:正文自己还在说「长期事实 / 反复规律 / 一份约定 /
// 主人明确要求记住」时不给结论,落回旧规则);`durable` / `recurring` 判 long
// 并**不再跑规则**;`unknown` 与未分型 ⇒ **没有结论**,落到下一步。
// 🔴 `unknown` 绝不等于「一次性」:不许因为正文看着像今天的事就判 temp
// (fail-safe 方向:宁可多留)。
// 4. **提取器的时间性元数据位**(迁移 0027、temporal.go):
// `durable` / `recurring` ⇒ long 并**不再跑规则**(模型已答「不是一次性事件」,
// 方向是 fail-safe:多留);`one_off` ⇒ **不作结论**,只把 N6 的位置判据放开
// (交给下一步的同一份规则跑,语义安全阀照旧);`unknown` / 未分型 ⇒ 规则照旧。
// 🔴 `one_off` **不是**「无条件 temp」:r1 复审裁定这一位只接管位置判据,
// 不接管语义判据(详见 temporal.go 头注)。🔴 `unknown` 也绝不等于「一次性」。
// 5. **留存规则命中**(design/03 §10.5 的 N1~N7)⇒ temp,并带回规则码。
// 6. 其余 ⇒ long(默认落点,fail-safe)。
//
@@ -125,10 +125,12 @@ func resolveRetention(item MemoryItem) (retentionVerdict, error) {
if nonOwnerSource(item.SourceConversation) {
return retentionVerdict{Tier: RetentionTierUnset}, nil
}
if verdict, ok := temporalVerdict(item); ok {
return verdict, nil
// 时间性位只有 `durable` / `recurring` 直接出结论;`one_off` 作为**输入**进规则层
// (放开 N6 的位置判据),因此下面这一行必须把 `item.Temporal` 传下去。
if tier, ok := temporalTier(item.Temporal); ok {
return retentionVerdict{Tier: tier}, nil
}
if rule := judgeExtractionRetention(item.Content); rule != retentionNone {
if rule := judgeExtractionRetention(item.Content, item.Temporal); rule != retentionNone {
return retentionVerdict{Tier: RetentionTemp, Rule: rule}, nil
}
return retentionVerdict{Tier: RetentionLong}, nil