Files
Cyrene_v2/internal/runtime/memory/temporal.go
T
AskaEth c7c0294a16 refactor(memory): M45 裁决 D —— temporal 位撤出判定,只保留观测
按 r1 复审裁决 D:这一位先只做观测,`resolveRetention` 不许读它。理由(实测):
无条件 one_off ⇒ temp 会当天删 21 条长期约定;只放开 N6 位置判据能翻的 13 条
逐条是约定、而复审点名的三条目标行 0/3 救得了。在「模型真实标签分布」这份证据
缺位时不该做 7 天后物理删的不可逆决定。

撤接线(结论集逐字回到 M45 之前):
- retention.go:resolveRetention 不再读 item.Temporal;与之对比 dev base
  (af61b8c)本文件**只有注释差异**,规则与判定入口逐字未变。
- extract_retention.go:judgeExtractionRetention 签名回退单参;删除
  judgeRetentionTimepointWide 与 n6_timepoint_wide 规则码。与 dev base 对比
  **同样只有注释差异** ⇒ 「结论集 = 不含 temporal 的那个集合」是结构性的,
  不是靠调用点记得别传。
- 连带取消初版的 durable / recurring ⇒ long(零影响必须是双向的)。

保留观测面(一律未动):迁移 0027 的列、自举前滚、提取器的 temporal 产出、
prompt 枚举、闭集与 normalizeTemporal、store_temporal.go 的校验/编码。
另外把 prompt 里「标成 one_off 会让它进临时层、到期消失」这句**撤接线后已是假话**
改成中性标注说明 —— 否则会诱导模型避开 one_off,观测数据直接被污染。

测试:
- TestTemporalBitDoesNotAffectRetention —— 4 类语料 × 5 个取值,(档位,规则码)
  与未分型逐字相同(核心正断言);
- TestTemporalOneOffDoesNotMakeAnythingTemporary / ...DoesNotRescueRuleHitRows
  —— 零影响的双向点名;
- TestTemporalIsStillRecorded —— one_off 的约定行仍落 temporal='one_off' 而档位
  long、expires 空(观测在、判定不动);
- TestTemporalDoesNotTouchExplicitTierOrRestrictedSource —— 人工入口与受限来源不变。
变异实测:把 `if item.Temporal == TemporalOneOff { return temp }` 插回
resolveRetention ⇒ 上述四条用例全红(FAIL 输出已贴给塔),恢复后全绿。

IngestRuleVersion 保持 d3.5,注释改准为「记录 temporal 信号,但不参与判定」
(留痕面变了:库里多了「模型当时标了什么」这一维,回退会让新旧留痕混在一起)。

P2 三条保留:store.go 500 行(拆 store_temporal.go)、temporal.go 头注('' 的口径
+ 两次被否的接线方案 + 接线前必须看的那两条统计 SQL)、报告语境段。

回归:go build ./... + go vet ./... 全绿;memory/db 包测试全绿;memory -race
38.2s 全绿。
2026-10-26 19:21:24 +08:00

109 lines
5.9 KiB
Go
Raw 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 "strings"
// 本文件是**提取器产出侧的时间性元数据位**(`temporal`,迁移 0027):它记录模型对
// 「这条记忆说的是**一次发生的事**,还是**反复 / 长期成立的事**」的判断。
//
// 当初为什么要这一位(M37 复审的结论,逐字):「真正卡住上游的是**提取器的改写**:
// 一次性事件被写成 30~80 字的第三人称长期陈述(同时带『近期/目前』和持久词),
// 规则闸看不到『这是今天的一次性事件』这一位。这不是阈值问题,是**判定链缺信息位**。」
//
// 🔴 **当前状态:只观测,不参与判定**(r1 复审裁决 D,2026-10-27)。`resolveRetention`
// **不读**这一位,判定的结论集与「没有这一位」逐字相同。保留它的唯一目的是**积累
// 证据**:等真实提取跑出一批标签之后,再回答「模型到底怎么标」以及「接进判定是赚
// 还是亏」。
//
// 为什么撤(两次候选接线都被生产重放否掉,别再凭直觉改回去):
//
// ① **无条件 `one_off ⇒ temp`**(任务书初版口径):最近 500 条 active 里人工按
// 提取器口径标出的 23 条 `one_off`,其中 21 条是「约定 + 某一次没走成」形态 ⇒
// 7 天后**把约定本身物理删掉**(不可撤回)。
// ② **只放开 N6 的位置判据**(r1 复审给的改法):能翻的行在窗口内 13 条、**逐条都是
// 约定**(`约定/约好 + 今天没走成`),而复审点名的三条目标行 **0/3** 救得了
// (`…睡得比平时早` 被习惯词、`…以后不用再惦记` 被永久化标记、`自行车链条` 那条
// 没有时点词)⇒ 收益为 0、风险集中在长期约定。
//
// 还有一层方法论上的理由:以上两轮数字**全部**建立在「对历史改写正文**人工**标注」
// 上 —— 生产库里 `temporal` 列当时还不存在,模型从未在这批正文上产出过标签。
// 用人工标签去决定一条「模型标签驱动」的接线,证据链是断的。
//
// 将来真要接线,前置条件是**这份数据**(不是又一次重放):
//
// SELECT temporal, COUNT(*) FROM memory_items GROUP BY temporal;
// SELECT created_at, retention_tier, content FROM memory_items
// WHERE temporal = 'one_off' ORDER BY created_at DESC LIMIT 10;
//
// 看 `one_off` 那 10 条里有多少是长期约定 —— 那才是「这一位能不能用」的判据。
//
// 闭集(与迁移 0027 的列值口径逐字一致):
//
// one_off 一次性事件(今天 / 这次 / 某一次发生的事)
// recurring 反复发生的规律(每天 / 每周 / 每次都…)
// durable 长期成立的事实 / 偏好 / 约定 / 称呼 / 身份
// unknown 模型拿不准,或没给出合法值
// '' 尚未分型:0027 之前的历史行 + 没做时间性判断的写入路径
// (`ManualExtractor` 的「记住 X」、网关/管理面手写、整理侧回填 ——
// 它们本来就由调用方显式给档位)
//
// 🔴 `unknown` 与 `”` 是两件事:`unknown` 是「问过模型了,它拿不准」;`”` 是
// 「这条路径没问过」。把两者折成一个值会让「模型到底答了什么」这个观测面当场消失,
// 而那正是保留这一位的全部意义。
type TemporalKind string
const (
// TemporalUnset 是「尚未分型」:历史行与没做时间性判断的写入路径(见文件头注)。
// **它不是业务取值**,也**不是**「unknown」的同义词。
TemporalUnset TemporalKind = ""
// TemporalOneOff 是一次性事件。它目前**不改变任何判定**:写入时只落库,
// 供事后统计「模型把哪些行标成了某一次发生的事」。
TemporalOneOff TemporalKind = "one_off"
// TemporalRecurring 是反复发生的规律(「每天散步十分钟」「每周三开会」)。
TemporalRecurring TemporalKind = "recurring"
// TemporalDurable 是长期成立的事实 / 偏好 / 约定 / 称呼 / 身份。
TemporalDurable TemporalKind = "durable"
// TemporalUnknown 是「问过模型了,它拿不准」。
TemporalUnknown TemporalKind = "unknown"
)
// valid 报告 t 是否落在闭集内(**含**未分型的空串:它是合法的列值,不是错误)。
func (t TemporalKind) valid() bool {
switch t {
case TemporalUnset, TemporalOneOff, TemporalRecurring, TemporalDurable, TemporalUnknown:
return true
default:
return false
}
}
// assigned 报告 t 是否**已由提取器分型**(即非空串)。它是「未分型」的唯一判据,
// 与 RetentionTier.assigned 同族,调用方不许用 `t == TemporalUnknown` 之类的
// 比较去替代它(那是「问过但拿不准」,不是「没问过」)。
func (t TemporalKind) assigned() bool { return t != TemporalUnset }
// normalizeTemporal 把模型输出的自由文本归一到上面四个值。
//
// 🔴 **只做大小写折叠与首尾空白裁剪,绝不做同义词猜测**(「一次性」「once」→ 都不认):
// 认不出来的答案就是解析失败,一律落 TemporalUnknown —— 这正是任务书
// 「模型只能选那四个值,解析失败一律归 unknown(绝不猜)」的字面实现。将来接线时
// 这条尤其重要:猜同义词的代价是单向的(猜成 one_off 就删记忆)。
func normalizeTemporal(s string) TemporalKind {
switch TemporalKind(strings.ToLower(strings.TrimSpace(s))) {
case TemporalOneOff:
return TemporalOneOff
case TemporalRecurring:
return TemporalRecurring
case TemporalDurable:
return TemporalDurable
default:
// 空值、非法值、拼写错误、模型自造的新值:全部归 unknown(**不报错**)。
// 一条候选记忆的价值不取决于它的时间性位是否写对,因此解析失败绝不
// 让整批提取失败(那会静默丢掉同批的其他候选)。
return TemporalUnknown
}
}