记忆 accessed_at 归一化

wesgine 记忆系统中 accessed_at 字段的归一化设计,确保零值不会导致 GC 和排序异常。


零值问题

accessed_at 列定义为 NOT NULL DEFAULT 0,Go 零值映射到 1970-01-01T00:00:00Z。然而这个字段决定了:

消费者零值影响
GC retentionScore时间衰减接近 0,优先被淘汰
Hybrid Ranker排序权重压到最低
Digest stale 判定被读作"30+ 天未被召回"→ 标记 stale
两处 ORDER BY accessed_at DESC排到查询末尾

零值不是中性状态,是该字段最坏的取值,且恰好落在最新写入的行上。


历史补偿

此前有 6 个读者各自带一份补偿逻辑:

if entry.AccessedAt.IsZero() {
    use entry.CreatedAt
}

而第 7 个消费者(context_index.go 的 ORDER BY)没有补偿——同一行在 5 处读作创建时间,在 1 处读作 epoch,不报错,只是在某一个面上永久排最后。


当前设计:归一一处,读者不补偿

saveCore 在落盘前执行归一:

if entry.AccessedAt.IsZero() {
    entry.AccessedAt = entry.CreatedAt
}

同时 ddlMemoriesV10 幂等修存量:

UPDATE wes_memories
SET accessed_at = created_at
WHERE accessed_at = 0;

缺任何一条,删掉那六份补偿就不安全。


为什么不用 DEFAULT CURRENT_TIMESTAMP

这会让所有未被召回的行的 accessed_at = 写入时间,混淆"刚写入"与"刚被召回"两种语义。created_at 是更精确的下界。


第二排序键

accessed_at 相同时用 created_at 作为 tiebreaker:

ORDER BY accessed_at DESC, created_at DESC, id

并列时不确定的顺序移动的是页边界——同一查询的返回行集随谁先入列而变。


不变量

INV-MEM-51:accessed_at 的下界是 created_at,归一只在 saveCore 一处。


相关文档