feat(memory): 整理不再只产建议 —— 轮末自动落地高置信 + 停产 link 候选
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人原话:「那么多 link 是干什么的?该删就删,怎么这个记忆整理机制这么保守呢」。

① **link 停产**(依据 design/23 §14 把它列为「未细化」⇒ 既无落点也无数据模型;
   `applyable()` 也只认 merge/archive/reclassify ⇒ 应用 link 恒 `skipped`,是纯噪音):
   - 规则层 `containNone` 判定改为 `keep`(不再产出 501 条不可应用的待办);
   - `TidyLink` 常量保留,只为**读历史行**与「不可应用」判定;
   - 存量出路:新端点 `POST /api/v1/admin/memories/candidates/reject?action=&limit=`
     (**幂等、只改候选 status、不碰记忆**),主会话用它清掉存量 501 条。
② **轮末自动落地**(默认开,仅两类,判据是纯函数):
   - `merge`:**字面重复** ∨「事实集合等价 ∧ 相似度 ≥ 0.95」;
   - `archive`:理由 = `one_off_event` **且不像习惯**(`design/23 §5.2-1` 红线:
     习惯类会因正文带钟点被误判成一次性事件,归档它是错的)。
   - **不自动**:`reclassify`、低置信 merge、跨分区/跨角色、任何改正文、受限来源
     (群聊/陌生人)—— 新增执行面守卫 `tidySkipSourceRestricted`。
③ **六条护栏**(缺一不可):软动作(复用既有 apply 路径)/ 每轮上限 50(硬顶 500,
   超限如实 `Truncated` + 真总数)/ 审计 `memory.tidy.apply`(`auto=true`,不带正文)/
   **可 revert**(复用既有回滚)/ 幂等(复用候选状态条件更新)/ **一键关回只产建议**
   (`$DATA_DIR/config/memory-tidy.json` 的 `autoApply:false`,**零新 env 键**)。

验收(主会话复跑 + 亲自变异):memory 与 gateway 两包 build/vet/test 全绿;我用
-overlay 短路「习惯不归档」守卫 ⇒ TestTidyAutoNeverArchivesHabitMisjudgedAsOneOff
变红(逐字报「作息习惯被自动归档了 …design §5.2-1 红线」)。

⚠️ 未部署(要 build+restart 才生效);⚠️ 契约欠登记(新端点 B25 + §7.2),主会话补;
⚠️ 真机端到端未跑(不能重启后端)。
This commit is contained in:
AskaEth committed 2026-10-07 11:07:00 +08:00
1 parent 353293e807
commit 6d92536276
27 files changed
+2063 -197

No files matched your search

+59
View File
@@ -24,6 +24,12 @@ const (
// TidyRuleVersion 是 T0 规则集的版本号,进候选的幂等键(design/23 §9):
// 规则不变时重复运行绝不重复产候选;升版后同一数据重新产候选,
// 因此被 reject 的对可以重评。**改判定规则必须同时升它**。
//
// ⚠️ 2026-10-07「link 不再产候选」**刻意没有**升它(不是漏了):升版的目的是
// 「让历史 reject 的对被重评」,而本次变更不改变任何一对/一条的**判定结论**
// (互不包含的对仍判 keep,只是不再落一条不可应用的候选);升版只会让活库里
// 87 条既存候选凭 idempotency_key 复制出一份重复行,与主人「该删就删」相反。
// 真正需要升版的是「判定结论会变」的改动(阈值语义、包含算法、否决规则)。
TidyRuleVersion = "t0.1"
// defaultTidyIntervalSec 是整理周期秒数,默认 86400 = 每天 1 次
@@ -81,6 +87,40 @@ const (
// maxTidyApplyBatchLimit 是批量应用上限的硬顶:请求给更大的值就钳到这里
// (与既有查询面「钳位而不是报错」同一约定)。
maxTidyApplyBatchLimit = 500
// defaultTidyAutoApplyPerRound 是**轮末自动落地**的每轮条数上限(2026-10-07
// 主人口径「该删就删,怎么这个记忆整理机制这么保守呢」)。
//
// 它是**代码默认值**,可被 `$DATA_DIR/config/memory-tidy.json` 的
// autoApplyPerRound 覆盖(`.env` 已冻结、design/13 §2.1:新旋钮一律走
// $DATA_DIR/config/** 或代码默认值,不进 .env)。
//
// 上限存在的理由与 design/23 §7 原本「merge/archive 永远人工确认」的顾虑同源:
// 自动落地是**批量改记忆**,一天一轮最多 50 条,坏判据最多伤 50 条且每条都能
// revert —— 而不是一次点下去改几百条、事后无从追溯。
defaultTidyAutoApplyPerRound = 50
// maxTidyAutoApplyScan 是轮末自动档一趟**读取**待定候选的条数上限。自动档要在
// Go 侧按判据筛(判据要读候选证据 JSON,SQL 表达不了),因此它必须有个上限;
// 命中上限时自动档只处理最新的一批,直到**下一轮**再继续(与扫描上限同一口径:
// 最新优先 + 绝不静默,见 tidy_auto.go 的 ScanTruncated 与那条 WARN)。
maxTidyAutoApplyScan = 2000
// tidyAutoMergeMinSimilarity 是自动合并第二档判据的相似度下限:**事实集合等价
// (双向包含 = containBoth)且相似度 ≥ 0.95**。合同等价但相似度低的对说明
// 「关键要素相同、表述差得远」,那不是重复而是两种说法,交人工。
tidyAutoMergeMinSimilarity = 0.95
// defaultTidyRejectBatchLimit 是「按动作批量拒绝候选」的单次条数上限
// (2026-10-07:给活库里 501 条无执行语义的 link 候选一个干净去处)。
//
// 它比批量应用的上限宽得多是**刻意的**:拒绝只写候选表的状态列,**不碰
// memory_items 任何一行**,因此没有「一次改坏几百条记忆」的风险;但仍有上限,
// 且超限如实回报 total/truncated(绝不静默少做)。
defaultTidyRejectBatchLimit = 1000
// maxTidyRejectBatchLimit 是上面那个上限的硬顶(钳位而不是报错)。
maxTidyRejectBatchLimit = 10000
)
// TidyConfig 携带记忆整理的调优旋钮。零值经 normalize 选取安全默认值,
@@ -109,6 +149,18 @@ type TidyConfig struct {
// CompareLimit 是每分区每轮真正参与两两比对的条目上限(性能护栏)。
// 非正用默认值(maxTidyCompareItems)。
CompareLimit int
// DisableAutoApply 关闭**轮末自动落地**(见 defaultTidyAutoApplyPerRound)。
//
// 它是**反向**字段,且这是刻意的:bool 的零值必须落在「默认行为」上 ——
// 自动落地是本机制的默认取向(2026-10-07 主人「该删就删」),若写成正向的
// AutoApply,任何没显式配过的地方(含测试与组合根构造的一半配置)都会被读成
// 「关掉」,与默认相反。要一键回到「只产建议」,写
// `$DATA_DIR/config/memory-tidy.json` 的 `{"autoApply": false}`(不加 env 键:
// `.env` 已冻结)。
DisableAutoApply bool
// AutoApplyPerRound 是轮末自动落地的**每轮**条数上限。非正用默认值
// (defaultTidyAutoApplyPerRound);最终还会被 maxTidyApplyBatchLimit 钳位。
AutoApplyPerRound int
}
// TidyConfigFromConfig 把 config.MemoryTidyConfig(env 侧旋钮)映射成
@@ -150,9 +202,16 @@ func normalizeTidyConfig(cfg TidyConfig) TidyConfig {
if cfg.CompareLimit <= 0 {
cfg.CompareLimit = maxTidyCompareItems
}
if cfg.AutoApplyPerRound <= 0 {
cfg.AutoApplyPerRound = defaultTidyAutoApplyPerRound
}
return cfg
}
// autoApplyEnabled 报告轮末自动落地是否开启。判据只有 DisableAutoApply 一个
// (见它的零值口径),因此「默认开启」是可证的一条,而不是注释里的承诺。
func (c TidyConfig) autoApplyEnabled() bool { return !c.DisableAutoApply }
// quietWindowDisabled 报告低峰窗口是否被显式关闭(两端相等且非 0)。
func (c TidyConfig) quietWindowDisabled() bool {
return c.QuietStartHour != 0 && c.QuietStartHour == c.QuietEndHour