主人 2026-10-26 对「记忆分型」裁定「两列」:kind(关于什么)保留,新增 retention_tier(留多久 / 干什么用)。本片把这条轴从列、判定、写入、读侧过滤 一路打通,并把「判第五态 defer」与「判 retention_tier=temp」合并成一个判定。 结构(迁移 0026 + 自举前滚): - memory_items 新增 retention_tier TEXT NOT NULL DEFAULT ''(空串 = 尚未分型, 历史行与受限来源行),索引 idx_memory_items_retention_tier;up/down 均实跑过 (在隔离的生产库副本上 migrate → 建 Store → down → 再 up,5073 行零丢失)。 - NewStore 幂等前滚该列 + 建索引(ensureRetentionSchema),与迁移同形由测试钉住。 判定(retention.go,全仓唯一入口): - RetentionTier 闭集 temp/daily/work/long/knowledge + 空串未分型;与 kind 正交。 - resolveRetention 的优先级:调用方显式档位 > 显式有效期(⇒ temp)> 受限来源 不分型 > 留存规则 N1~N7 ⇒ temp > 默认 long。判据靠元数据,规则不许推翻显式档位。 - tempDeadline 是「档位 → expires_at」的唯一换算:temp ⇔ 有效期非空,两列同一笔 INSERT 落下(M2 登记过的「INSERT 与补写之间的短窗口」就此关闭)。 合并(defer ⇔ temp): - judgeIngest 不再跑留存规则,只读 Add 落定的 item.RetentionTier;规则调用点因此 全仓唯一(源码级断言钉住)。 - 判定不再依赖同步闸是否开启:闸未接/关着时档位与有效期照样落库。 - 异步层对已是 temp 的行不重复改写(避免 TTL 随审查延迟漂移)。 - IngestRuleVersion d3.3 → d3.4(判据作用范围变了,按既有硬规矩升版)。 读侧: - 四条列表读路径的 SELECT 与 scanItem 读出 retention_tier(14 列);Get 同样读回。 - 新增 ListByPersonaTiers / ListScopeTiers / ListTiers / ListProjectTiers(可选档位 过滤,缺省不过滤 ⇒ 历史行照常返回;显式传空串只取未分型;非法档位报错)。 原方法签名逐字不变 —— 网关 MemoryStore 接口钉着它们,可变参数会编译不过。 - store.go 的读路径拆到 store_list.go,守 500 行上限。 回归:go build ./... + go vet ./... + go test ./... 全绿(内存包另有 -race)。
49 lines
2.3 KiB
Go
49 lines
2.3 KiB
Go
package memory
|
||
|
||
// schema DDL。internal/db 中的迁移框架是领域表的权威
|
||
// 机制,但这里刻意不用它:db 包的迁移测试断言精确的
|
||
// 迁移数量,而记忆存储必须自包含、
|
||
// 可在任意 *sql.DB 上使用。表与索引因此
|
||
// 由 NewStore 幂等创建。
|
||
const createMemoryItemsTable = `
|
||
CREATE TABLE IF NOT EXISTS memory_items (
|
||
id TEXT PRIMARY KEY,
|
||
persona_id TEXT NOT NULL,
|
||
role_id TEXT NOT NULL DEFAULT '',
|
||
project_id TEXT NOT NULL DEFAULT '',
|
||
scope TEXT NOT NULL,
|
||
kind TEXT NOT NULL,
|
||
content TEXT NOT NULL,
|
||
tags TEXT NOT NULL DEFAULT '[]',
|
||
confidence REAL NOT NULL DEFAULT 1.0,
|
||
source_conversation TEXT NOT NULL DEFAULT '',
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL,
|
||
decay_weight REAL NOT NULL DEFAULT 1.0,
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
merged_into TEXT NOT NULL DEFAULT '',
|
||
merged_from TEXT NOT NULL DEFAULT '[]',
|
||
merge_count INTEGER NOT NULL DEFAULT 1,
|
||
occurred_at TEXT NOT NULL DEFAULT '',
|
||
valid_to TEXT NOT NULL DEFAULT '',
|
||
expires_at TEXT NOT NULL DEFAULT '',
|
||
retention_tier TEXT NOT NULL DEFAULT ''
|
||
)`
|
||
|
||
const createMemoryItemsPersonaIndex = `
|
||
CREATE INDEX IF NOT EXISTS idx_memory_items_persona_id
|
||
ON memory_items (persona_id)`
|
||
|
||
const createMemoryItemsPersonaCreatedIndex = `
|
||
CREATE INDEX IF NOT EXISTS idx_memory_items_persona_id_created_at
|
||
ON memory_items (persona_id, created_at)`
|
||
|
||
// createMemoryItemsRetentionTierIndex 是「留多久 / 干什么用」轴的读侧索引(迁移 0026)。
|
||
// 三条列表读路径的可选档位过滤(`retention_tier IN (...)`)与「只看未分型的行」
|
||
// (回填分型的入口)都吃它。列**必须**排在 CREATE TABLE 的最后:迁移的
|
||
// `ALTER TABLE ADD COLUMN` 也把新列追加在末尾,两份 DDL 才逐列同形
|
||
// (retention_migration_test.go 钉住这条同形,少一列就是「新库有、老库无」的漂移)。
|
||
const createMemoryItemsRetentionTierIndex = `
|
||
CREATE INDEX IF NOT EXISTS idx_memory_items_retention_tier
|
||
ON memory_items (retention_tier)`
|