Files
Cyrene_v2/internal/runtime/memory/preinsert_tuning.go
T
AskaEth 21b906ffb0 feat(memory): D3 第五态 defer(入库审查)+ 提取留存规则闸 + 临时 TTL 配置面
主人原话:「收窄提取器,确实要给他一个主动式的记忆留存规则,不能想到什么都
往记忆里写」「.env 只做兜底用 …这些键优先从面板配置上取」「临时记忆到期删除」。
按 design/23 §18(D3 第五态 defer)、§19(到期处置)、design/03 §9.6
(tempTtlSec 默认 7 天 + 坏值口径)、§10(提取留存规则)落地。

- IngestDecision 闭集加第五态 defer:照常 active 落库 + 写短 expires_at 落
  临时层(不删行、不改正文,回滚 = 清空该列);IngestRuleVersion d3.2 → d3.3。
- 落点:ingestGatePlan 增 ExpiresAt + expiresAt()(走 expiryText/formatTime 的
  定宽 30 字节 UTC,与读侧时效闸同一套文本口径),由 finishIngest 在 defer 一态
  补写 expires_at(INSERT 与补写之间的短暂窗口如实登记,方向 = 宁可多留)。
  非 defer 三态一笔都不多写。
- T0 提取留存规则闸(extract_retention.go):design/03 §10.5 的 N1~N5 纯规则,
  接进 judgeIngest 的候选比对之前,命中即 defer + reason=t0_low_value,不进 T1;
  两道护栏(显式「记住」/ 持久信号)一票放行,fail-safe 方向 = 宁可多留。
- T1 闭集扩为 merge/modify/keep/defer(同一个 prompt、同一次调用,绝不新增
  第二次模型调用):只有关系判为真新时才问价值;defer 要求 target=0,否则判
  非法回落 create(reason=t1_reply_invalid)。
- 提取器提示词加固(design/03 §10.3 B):正向清单 + 反例清单 + 「允许返回 []」。
- 临时记忆 TTL:代码默认 tempTtlSec = 604800(7 天);面板
  $DATA_DIR/config/memory-tidy.json 的 tempTtlSec 优先(ApplyReviewTuning 逐键
  解析,坏值回落默认 + WARN,绝不写进 cfg)。该键在 .env 里没有出处且不新增
  (.env 已冻结)⇒ 实际生效链是「面板 > 代码默认」。启动播种同步该键(*int +
  omitempty,nil 省略、绝不写 null),保证播种文件与生效值逐字一致。
- preinsert_store.go 的 listReviewCandidatesSQL 原位注释:这条内部召回 SQL
  刻意不过时效闸的裁定与理由(design/03 §11.2 第 3 条)。
- 测试:defer 落库带短 expires_at(闸 + T1 两条路径)、写入口径与时效闸同源、
  TTL 坏值与回落、规则版本 d3.3、整理候选幂等键不含 IngestRuleVersion、
  提取规则正例 11 条 / 反例 11 条。

验收:go build ./... + go vet ./... + go test ./... 全绿;windows/darwin 交叉
编译通过(GOOS=darwin GOARCH=arm64 / GOOS=windows GOARCH=amd64)。
2026-10-13 20:22:48 +08:00

71 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package memory
import (
"log/slog"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// 本文件是**入库前审查调优面**的解析点:把 `$DATA_DIR/config/memory-tidy.json`
// 的面板旋钮叠到 ReviewConfig 上 —— 与 tidy_tuning.go 对 TidyConfig 做的事同源、
// 同一条口径(主人 2026-10-07:「**`.env` 只做兜底用** —— 后端自动创建独立的面板
// 配置文件之后就走面板配置文件,这些键优先从面板配置上取」)。
//
// 为什么解析放在 memory 而不是 runtimeconfig:runtimeconfig 只负责**文件形状与
// 读取**(它的头注释把这条边界写死了),而「缺位/坏值怎么回落」属于 ReviewConfig
// 的归一化语义 —— 与 normalizeReviewConfig 同一层,因此两者可以一起被单测覆盖。
//
// 为什么不是「读盘时逐键容错」:整份文件解不开(含某键类型写错)时**全部键一起**
// 回落,这是本文件家族既有的粒度(见 runtimeconfig.LoadMemoryTidyTuning 与它的
// 用例③);本文件处理的是**类型正确但值不可用**(非正数)那一档。
// ApplyReviewTuning 把面板调优旋钮叠到 ReviewConfig 上,返回叠加后的配置
// (**未归一化**:调用方随后交给 NewPreInsertReviewer / normalizeReviewConfig
// 填代码默认值,与组合根对 TidyConfig 的既有顺序一致)。
//
// 目前只有一键,逐键独立,三档优先级是:
//
// 键 面板(memory-tidy.json) 下一档(.env) 代码默认
// tempTtlSec tempTtlSec **无此键**(结构性为空) 604800(7 天)
//
// 🔴 **中档(`.env`)在结构上是空的**,这是**刻意**的、不是漏配:`.env` 已冻结
// (design/13 §2.1「不加、不改、不删」),而「多久算临时」是本次新增的能力,
// 它在 `.env` 里**从来就没有键**。因此实际生效链是「面板 > 代码默认」——报回报时
// 不许把它写成「面板 > .env > 代码默认」而让读者以为 `.env` 那档还有东西可调。
//
// 坏值(非正秒数)**绝不写进 cfg**:回落代码默认 + 记一条 WARN。一个 ≤0 的 TTL
// 会让每条 defer 记忆**落库即过期**(读侧立刻隐藏、后台清扫下一轮就删)——那是
// 坏配置而不是「不过期」,但也不许因此让启动失败(design/03 §9.6)。
func ApplyReviewTuning(cfg ReviewConfig, panel runtimeconfig.MemoryTidyTuning, log *slog.Logger) ReviewConfig {
log = logger.WithSource(baseTidyLog(log), "memory/review")
if sec := panelPositiveTTLSec(panel.TempTtlSec, "tempTtlSec", log); sec > 0 {
cfg.TempTTL = time.Duration(sec) * time.Second
}
return cfg
}
// panelPositiveTTLSec 解析**临时记忆 TTL** 旋钮,返回 0 表示「不覆盖」(调用方原样
// 留着上一档的值,最终由 normalizeReviewConfig 落到代码默认 7 天)。
//
// 三种情形分得很清,因为它们该有不同的观测形状(与 tidy_tuning.go 的
// panelPositiveSec 逐字同构):
//
// - `nil`(键没写)⇒ 0,**静默**:没配不是错,正是「代码默认兜底」那一档;
// - 非正数(写了 0 或负数)⇒ 0 + **WARN**:它会让每条临时记忆落库即过期
// (与「不过期」恰好相反),属于坏配置,必须留下痕迹;
// - 正数 ⇒ 原值。
func panelPositiveTTLSec(v *int, key string, log *slog.Logger) int {
if v == nil {
return 0
}
if *v <= 0 {
log.Warn("memory-tidy.json 的临时记忆 TTL 不可用,回落代码默认(非正 TTL 会让临时记忆落库即过期)",
"key", key, "value", *v, "fallback_sec", tempTtlSec)
return 0
}
return *v
}