生产启动行仍是 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 层;注释里已写明「比常量」与「比组装后生效值」的区别(防后人重蹈)。
77 lines
4.5 KiB
Go
77 lines
4.5 KiB
Go
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
|
||
}
|