feat(memory): 留痕表正式迁移 0025 + 保留期清扫(W5)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 迁移 0025_memory_ingest_reviews(幂等,既有部署重跑不炸/不丢行)
- GC:保留期 720h + 条数上限 10 万行,挂进审查服务生命周期(Start 起 / Stop 停 / 启动先扫)
- 过期即硬删(RevertMerged 只读 memory_items,删留痕不损失回滚能力)
- 偏离一处并给理由:表只留 created_at(先例 0017;append-only;加 updated_at 需三份 DDL 真源)
- 3 个变异自检(边界 <=/元组比较方向/Retention=0)
This commit is contained in:
AskaEth committed 2026-10-06 15:59:16 +08:00
1 parent ddd3d00d09
commit ffe2518bf4
7 files changed
+861 -9

No files matched your search

@@ -0,0 +1,14 @@
-- 0025 的回滚:先删索引、再删表(与 0019 / 0021 的回滚同一顺序口径)。
--
-- 删留痕**不影响任何一条记忆的可逆性**:留痕描述的那次软合并早已落在
-- `memory_items` 上(`status` / `merged_into` / `merged_from` / `merge_count`),
-- 回滚一次合并(`Store.RevertMerged`)只读 `memory_items`(见
-- internal/runtime/memory/preinsert_store.go 的 selectMergedIntoSQL),
-- 从不读本表。代价只是「这次决定当初为什么这么判」的依据丢失,属降级可接受。
--
-- 且本表由记忆存储自举(`NewStore` → `ensureIngestReviewSchema`):回滚删表之后,
-- 下次启动会按同一 DDL 重建一张空表,不需要人工干预(这也是本表与
-- memory_items 这类「用户数据表」的关键区别 —— 它是派生的观测留痕,不是正文)。
DROP INDEX IF EXISTS idx_memory_ingest_reviews_created_at;
DROP INDEX IF EXISTS idx_memory_ingest_reviews_item_id;
DROP TABLE IF EXISTS memory_ingest_reviews;
@@ -0,0 +1,59 @@
-- 0025_memory_ingest_reviews: 入库前审查(design/23 §15.1 D3)留痕表的**正式迁移号**
-- (切片 W5;D3 本体已落地于 6e28b38,当时**刻意没占迁移号**,以免与并行切片抢号)。
--
-- 为什么现在补:本表此前**只**由 `memory.NewStore` 的幂等前滚创建
-- (internal/runtime/memory/preinsert_schema.go)。后果是「库里有这张表」这件事
-- 分裂成两份真源:新库靠自举、迁移账本却不知道它存在;而清理(保留期清扫)要按
-- `created_at` 扫表 —— 一个不在迁移账本里的表,没人能回答「它的形状是谁负责的」。
-- 本迁移把它登记进 schema_migrations(手法照 0019_memory_tidy 对 memory_items
-- 整理列 / 候选表 / 运行表的处理)。
--
-- 🔴 **幂等是硬要求**(本迁移的部署面比一般迁移宽):**所有既有部署的
-- `memory_ingest_reviews` 早已存在**(NewStore 自举),只是没有 0025 这一行。
-- 因此 DDL 一律 `IF NOT EXISTS`:重复执行 = 空操作,绝不因为「表已存在」而炸,
-- 也不会因为「表已存在」而漏建索引。形状必须与自举 DDL **逐字同形**(少一列就是
-- 「新库有、老库无」的漂移),由 `internal/runtime/memory/preinsert_migration_test.go`
-- 的对照测试钉住(迁移建的表 vs 自举建的表,列与索引逐项比对)。
--
-- 命名口径:表名复数、主键 `id`、索引 `idx_{table}_{col}`(援引 AGENTS §10 已登记例外,
-- 与既有迁移一致)。
--
-- 时间戳:**只有 created_at**(定宽 UTC 纳秒串,由 internal/runtime/memory.formatTime
-- 写入;见该包 timefmt.go 的口径)。本表是 **append-only**:留痕落一行即不再改写,
-- 因此按同类逐条样本表 `0017_llm_usage` 的先例**不引入 updated_at** —— 一个永远等于
-- created_at 的列只会撒谎,而且要补它就必须为既有部署再加一条幂等 ALTER,
-- 把 DDL 的两份真源变成三份。若将来真出现「留痕被后续动作改写」的需求,
-- 那是 shape 变更,按项目惯例开新迁移号。
--
-- 脱敏(design/23 §15 D3-8「记忆正文不进日志」延伸到表结构):本表**没有**承载
-- 记忆正文的列 —— 命中的既有 id(target_id / merged_ids)足以让 owner-only 的
-- 读侧回表取原文,而正文一个字都不进这张表。
CREATE TABLE IF NOT EXISTS memory_ingest_reviews (
id TEXT PRIMARY KEY,
item_id TEXT NOT NULL,
persona_id TEXT NOT NULL,
scope TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
decision TEXT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
veto TEXT NOT NULL DEFAULT '',
target_id TEXT NOT NULL DEFAULT '',
merged_ids TEXT NOT NULL DEFAULT '[]',
score REAL NOT NULL DEFAULT 0,
recall_mode TEXT NOT NULL DEFAULT '',
degraded INTEGER NOT NULL DEFAULT 0,
rule_version TEXT NOT NULL DEFAULT '',
session_id TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
-- 按被审条目回溯(「这一条记忆当初被谁合了」)。
CREATE INDEX IF NOT EXISTS idx_memory_ingest_reviews_item_id
ON memory_ingest_reviews (item_id);
-- 读侧倒序(ListIngestReviews)与**保留期清扫的唯一依赖**:批删语句带
-- `WHERE created_at < ? ORDER BY created_at LIMIT ?`,吃得上这条索引,
-- 且每批的扫描面有界(internal/runtime/memory/preinsert_gc.go)。
CREATE INDEX IF NOT EXISTS idx_memory_ingest_reviews_created_at
ON memory_ingest_reviews (created_at);