-- 0026_memory_retention_tier:「留多久 / 干什么用」这根轴(主人 2026-10-26 裁定「两列」)。 -- -- 为什么是**新列**而不是复用 kind:`kind`(relationship / world_event)回答的是 -- 「关于什么」,`scope`(user / persona / project)回答的是「谁的」,本条新增的 -- `retention_tier` 回答的是「留多久 / 干什么用」。三根轴**正交、互不推导**: -- 一条关系记忆可以是临时记忆,也可以是长期记忆;把「临时性」塞进 `kind` 会让 -- 「一条关系记忆又是临时的」无法表达(审计 `记忆存量体检-2026-10-26.md` 卷四的 -- 三条路对比,主人选了第 1 条)。 -- -- 🔴 默认值 `''` = **尚未分型**,不是第六档业务档位: -- ① 本迁移之前的历史行全部落在 `''`(本迁移**刻意不做回填**:那是一次写全表的 -- 判定动作,属于整理侧的第二现场,不该藏在迁移里,也不该在启动路径上静默跑); -- ② 受限来源(群聊 / 陌生人)的新行同样不分型(它们的留存由来源面决定,后台到期 -- 清扫也会跳过它们 —— 判成 temp 就是造一条「对读面隐藏却永不删除」的行)。 -- 读侧因此**必须**能区分「未分型」与「已分型」:缺省不过滤(历史行照常返回), -- 显式按 `''` 过滤即「只看未分型的行」(回填分型的入口)。 -- -- 档位闭集(代码侧的定义是 internal/runtime/memory/retention.go 的 RetentionTier): -- temp 临时层:唯一带时效的档位,`retention_tier='temp'` ⇔ `expires_at` -- 非空且落在临时 TTL 内(两列由同一个判定、同一笔 INSERT 落下) -- daily 日常层(一天之内的琐事 / 生活节奏;**不是** scope=persona 的「角色日常」) -- work 工作层(任务推进 / 工单 / 项目过程) -- long 长期层(稳定的偏好 / 关系 / 事实;入库判定的默认落点) -- knowledge 知识库层(外部资料 / 规格 / 文档性质的记忆行) -- '' 尚未分型(见上) -- -- 幂等性:`ALTER TABLE ADD COLUMN` 在 SQLite 里**没有** IF NOT EXISTS 形式,因此本 -- 迁移与 0019_memory_tidy 同一手法 —— 依赖启动顺序「先 MigrateUp、后 NewStore」 -- (cmd/cyrene/wire.go):生产库里这一步先跑,随后 NewStore 的自举前滚 -- (ensureRetentionTierColumn)走到 "duplicate column name" 是预期路径。反过来 -- (先自举、后迁移)也只在同一条账本跳一次。列与索引的形状必须与自举 DDL -- (internal/runtime/memory/store_schema.go)**逐字同形**,由 -- internal/runtime/memory/retention_migration_test.go 逐列/逐索引钉住。 -- -- 命名口径:表名复数、主键 `id`、索引 `idx_{table}_{col}`(援引 AGENTS §10 已登记 -- 例外,与既有迁移一致)。 ALTER TABLE memory_items ADD COLUMN retention_tier TEXT NOT NULL DEFAULT ''; -- 三条列表读路径的可选档位过滤(`retention_tier IN (...)`)与「只看未分型的行」 -- (回填入口)都吃这条索引。单列而不是复合列:这一轴的查询形状是 -- 「跨角色按档位聚合」(面板 / 回填),不绑定 persona。 CREATE INDEX IF NOT EXISTS idx_memory_items_retention_tier ON memory_items (retention_tier);