Files
Cyrene_v2/internal/config/memory_review.go
T
AskaEth 42a07cb454 fix(config): 单条入库审查的墙钟上限收敛到唯一真源(M16)
生产启动行仍是 timeout=20s —— M15 把 memory 包的 defaultReviewTimeout
抬到 90s 的修复**从未生效**:config 层的 env tag 自己带着 default:"20",
env 层永远产出非零值,于是 memory 侧「≤0 才回落默认」的归一化成了死代码。

修法(二选一中的「① 让 memory 侧成为唯一真源」):去掉 config 侧
MEMORY_REVIEW_TIMEOUT_SEC 的字面默认值,未配置时读出 0 = 「没配」,
由 normalizeReviewConfig 单点落地 90s。

生效值:
  .env / env 没写这个键 ⇒ 90s(唯一真源)
  写了正数            ⇒ 原样生效(不做任何「保底抬到 90」的加码)
  写了 0 / 负数       ⇒ 90s(与项目其它旋钮同一口径:≤0 = 用默认)

新增测试(走真实组装路径,不绕过任何一层):
  config.LoadMemoryReviewConfig → ReviewConfig.Timeout →
  NewPreInsertReviewer → normalizeReviewConfig → 生效值(= 启动行打印的 timeout=)
断言生效值严格大于「实测最坏召回链 57s + T1 子超时 8s」,并断言启动行打印它。
变异自证(修前红):把 tag 的 default:"20" 加回去 ⇒ 报出
  「组装后 timeout = 20s」+「生效值 20s != 唯一真源 1m30s」

原有 TestEmbeddingTimeoutFitsReviewBudget 拦不住这条,因为它比的是**本包常量**,
看不见 env 层;注释里已写明「比常量」与「比组装后生效值」的区别(防后人重蹈)。
2026-10-13 23:09:19 +08:00

77 lines
4.5 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 config
import (
"fmt"
"reflect"
)
// MemoryReviewConfig 是 D3「入库前审查」(design/23-记忆整理与去重 §15.1 D3)
// 在配置层的旋钮:新记忆落库后,由独立的记忆管理会话异步判定它是
// merge / modify / create / skip。
//
// 为什么用独立加载器而不是挂进 Config 根结构:Config 由 Load 统一解码,而
// 本切片刻意**不碰** Config / 记忆整理那组键(并行切片正在改它们)。这里复用
// 与 LoadLLMDefaults 同一手法(parseDotEnv + 同一份 `env:"…"` 解码器),
// 因此优先级与 Load 逐字一致:进程环境变量 > `.env` > 标签默认值(TimeoutSec
// 没有标签默认值,见该字段的注释 —— 未配置时它读出 0,由 memory 包定其生效值)。
// envPath 为空表示不看 `.env`(与 Load 的约定一致)。
type MemoryReviewConfig struct {
// Enabled 是入库前审查总开关,默认 true。依据:主人 2026-10-05 的裁定
// 就是「入库之前先走记忆管理会话审查,而不是反复新建」,而 T0 判定是
// **纯规则、零模型调用**,默认开启是安全的增量。置 false 时组合根不建、
// 不接、不跑 —— 写入路径行为逐字回到本次改动之前。
Enabled bool `env:"MEMORY_REVIEW_ENABLED" default:"true"`
// QueueSize 是有界审查队列的长度。队列满时该条**不做审查**:记忆本身
// 早已落库(绝不因队列满而丢记忆),只记一条带来源的 WARN。
QueueSize int `env:"MEMORY_REVIEW_QUEUE_SIZE" default:"256"`
// ScanLimit 是每次审查在同一分区内最多看多少条既有记忆(top-K 上限,
// design/23 §15 D3-4「每条待写记忆的审查有界」)。
ScanLimit int `env:"MEMORY_REVIEW_SCAN_LIMIT" default:"200"`
// TimeoutSec 是单条审查的墙钟上限(秒)。超时 ⇒ **不做任何改写**,
// 记忆照旧保留 + WARN(design/23 §15 D3-5 的 fail-safe)。
//
// 🔴 **本字段刻意没有 `default:` 标签**(2026-10-13,切片 M16)。单条审查预算的
// **唯一真源**是 memory 包的 `defaultReviewTimeout`(90s,推导见
// internal/runtime/memory/preinsert_config.go 的文件头注释)。这里原写
// `default:"20"`,后果不是「默认值是 20」而是**默认值根本不由 memory 侧决定**:
// env 层永远产出一个非零值 ⇒ memory 侧那道「≤0 才回落默认」的归一化成了**死代码**
// (90s 从未生效,生产启动行 `memory ingest review started … timeout=20s` 即铁证,
// 而实测召回链就要 21~23s ⇒ 每条候选满额的审查都超时、连留痕都不写)。
//
// 去掉字面默认后的**生效值**(路径:本字段 → cmd/cyrene/wire_memory_review.go 的
// `ReviewConfig.Timeout` → `normalizeReviewConfig`):
//
// `.env` / 进程环境没写这个键 ⇒ 这里 0 ⇒ memory 侧回落 90s
// 写了正数 ⇒ 原样生效(归一化只动 ≤0)
// 写了 0 / 负数 ⇒ 回落 90s(与项目其它旋钮同一口径:≤0 = 用默认)
//
// ⚠️ 别在这里重新加回字面默认值 —— 那会让 memory 侧的 90s 再次变成死代码,
// 而 memory 包的 TestReviewTimeoutEffectiveValueIsTheOnlyDefault 会立刻变红
// (它走完整组装路径取生效值,不比对常量)。
TimeoutSec int `env:"MEMORY_REVIEW_TIMEOUT_SEC"`
}
// LoadMemoryReviewConfig 读取入库前审查的旋钮。非法(负数)值不在这里报错,
// 而是与项目其它旋钮同一口径 —— **≤0 视为「用默认」**,由 memory 包 normalize
// 收敛,避免一个笔误让进程起不来。
//
// 🔴 因此本函数**对 TimeoutSec 不做任何回落**(切片 M16):返回 0 就是「没配」,
// 调用方必须原样交给 memory 包的 ReviewConfig(组合根今天就是这么做的),
// 「0 ⇒ 多少秒」由 `normalizeReviewConfig` 在那**唯一一处**决定。调用方若在这里
// 又补一个自己的默认值,全链就会重新长出第二个真源 —— 那正是本切片修掉的形态。
func LoadMemoryReviewConfig(envPath string) (MemoryReviewConfig, error) {
fromEnv := map[string]string{}
if envPath != "" {
m, err := parseDotEnv(envPath)
if err != nil {
return MemoryReviewConfig{}, fmt.Errorf("config: read %s: %w", envPath, err)
}
fromEnv = m
}
var cfg MemoryReviewConfig
if err := decodeValue(reflect.ValueOf(&cfg), fromEnv); err != nil {
return MemoryReviewConfig{}, err
}
return cfg, nil
}