// memorytidy.go 是记忆整理(design/23)**运行期调优**的可变源: // `$DATA_DIR/config/memory-tidy.json`(design/13 §2.1「运行期生成的配置进 // `$DATA_DIR/config/`,新增配置家族从一开始就只写这里,不落老位置」)。 // // 为什么需要它(2026-10-07 主人「那么多 link 是干什么的?该删就删,怎么这个记忆 // 整理机制这么保守呢」):轮末自动落地是一件**改记忆**的动作,它必须有一个 // 「一键回到只产建议」的开关与一个每轮上限,而 `.env` 已被主人冻结 // (`design/13 §2.1`:调优/门槛/上限/开关类参数走 `$DATA_DIR/config/**` + 代码 // 默认值,不进 `.env`)。于是这两项落在这里,**零新 env 键**。 // // 2026-10-07 追加 `ingestGate`(同步入库闸,主人「把记忆整理功能接到昔涟和记忆库 // 中间做审查,由整理功能进行入库」):同一个理由 —— 闸是一处**改记忆落库形态**的 // 开关,必须能一键回到「只插不审」,而 `.env` 已冻结 ⇒ 它落在同一个文件里, // 不新开文件家族、不加 env 键。 // // 2026-10-07 再追加 `t1Judge`(T1 灰区模型判定,design/23 §15.1 D3-2):理由仍然 // 是同一条 —— 它是一处**会花掉模型额度**的开关,必须能一行配置关掉(关掉即回落 // 纯规则、零模型调用),所以同样落在这里,不新开文件家族、不加 env 键。 // // 边界与 backthink.go 同源(那是同一族的手续): // // 1. 本文件只管**文件形状 + 读取**:它不认识 `TidyConfig`,也不认识「零值回落 // 代码默认值」那条裁决 —— 那是 `memory.normalizeTidyConfig` 的事。 // 2. **只读**:面板写路径不在本切片内,因此刻意没有 `Save*`。改动仍要 // **人手改这个文件 + 重启**(与 `backthink.json` 同口径:启动期读一次)。 // 3. **零新 env 键**,也不进面板的配置事实卡(那张表的键名被 `.env.example` 钉住)。 // // 读取口径:文件缺失或全空白 = 还没配过(零值 + nil,**不是错误**);存在但解不开 // = 返回错误,调用方记 WARN 并回落**代码默认值**(即:坏配置绝不能把自动档的开关 // 或上限读成「无限制」)。 package runtimeconfig import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" ) const ( // memoryTidyFileName 是文件名(家族命名 = 服务/模块名 + .json)。 memoryTidyFileName = "memory-tidy.json" // memoryTidyConfigSubdir 是落点的第一段目录(`$DATA_DIR/config/`)。与 // backthink 各自声明一份而不是共用一个常量:某一族换位置时,共用常量会让 // 另一处**静默**跟着搬家。 memoryTidyConfigSubdir = "config" ) // MemoryTidyConfigRelPath 是落点相对 `$DATA_DIR` 的路径(`config/memory-tidy.json`)。 // 导出它是为了让面板/文档能**如实**告诉主人改哪个文件,而不必把部署相关的绝对路径 // 塞进接口形状。 const MemoryTidyConfigRelPath = memoryTidyConfigSubdir + "/" + memoryTidyFileName // ErrInvalidMemoryTidy 是 memory-tidy.json 结构不合法时的分类错误(errors.Is 可判类)。 var ErrInvalidMemoryTidy = errors.New("runtimeconfig: invalid memory tidy config") // MemoryTidyTuning 是 `memory-tidy.json` 的形状:记忆模块运行期的旋钮。 // 文件以「整理」命名,但它是**记忆模块运行期调优的同一处落点**(整理自动档 + // 入库闸 + T1 灰区判定),不再为一个开关新开一个文件家族。 // // AutoApply / IngestGate / T1Judge 用**指针**是刻意的:它们的代码默认值都是「开」,而 // JSON 的 `bool` 零值也是 false —— 用 bool 会让「这个键没写」与「显式写 // false」不可区分,于是任何只写上限的文件都会**静默把开关关掉**。指针让 // 「没写」= nil = 不覆盖默认。 // // 字段名是 camelCase 的 JSON 键(文件是给人看、给人改的)。 type MemoryTidyTuning struct { // AutoApply 控制轮末自动落地。nil = 没配(用代码默认值:开);false = 一键 // 回到「只产建议」;true = 显式开(与默认同义,写它只是为了自解释)。 AutoApply *bool `json:"autoApply"` // AutoApplyPerRound 是每轮自动落地的条数上限。零/负 = 没配 ⇒ 用代码默认值 // (**没有「无上限」语义**,最终还会被硬顶钳位)。 AutoApplyPerRound int `json:"autoApplyPerRound"` // IngestGate 控制**同步入库闸**(D3 审查接在写入路径上当场拍板,主人 // 2026-10-07 裁定)。nil = 没配(用代码默认值:开);false = 一键回到 // 「只插不审」(= 改动之前的异步行为);true = 显式开(与默认同义)。 IngestGate *bool `json:"ingestGate"` // T1Judge 控制 **T1 灰区模型判定**(design/23 §15.1 D3-2:灰区 —— 相似 // 但互不包含 —— 调一次轻量模型,只许回 `merge`/`modify`/`keep` + 理由、 // **禁产新内容**)。nil = 没配(用代码默认值:开);false = 一键回到纯规则 // (**零模型调用**,灰区仍然保守回落新建);true = 显式开(与默认同义)。 // // 2026-10-07 追加,与 IngestGate 同一个文件、同一个理由:它是一处**会花掉 // 模型额度**的开关,必须能一行配置关掉,而 `.env` 已冻结(零新 env 键)。 // 指针也是同一个理由:代码默认值是「开」,用 bool 会让「这个键没写」与 // 「显式写 false」不可区分。 T1Judge *bool `json:"t1Judge"` } // AutoApplyOff 报告自动档是否被这份配置**显式关掉**。nil(没配)返回 false —— // 「没配」不等于「关掉」,那正是指针存在的理由。 func (t MemoryTidyTuning) AutoApplyOff() bool { return t.AutoApply != nil && !*t.AutoApply } // IngestGateOff 报告**同步入库闸**是否被这份配置显式关掉(口径同 AutoApplyOff: // nil 是「没配」⇒ 用代码默认的「开」,不是「关」)。 func (t MemoryTidyTuning) IngestGateOff() bool { return t.IngestGate != nil && !*t.IngestGate } // T1JudgeOff 报告 **T1 灰区模型判定**是否被这份配置显式关掉(口径同 AutoApplyOff: // nil 是「没配」⇒ 用代码默认的「开」,不是「关」)。 func (t MemoryTidyTuning) T1JudgeOff() bool { return t.T1Judge != nil && !*t.T1Judge } // DefaultMemoryTidyPath 返回记忆整理调优的落点:`$DATA_DIR/config/memory-tidy.json`。 func DefaultMemoryTidyPath(dataDir string) string { return filepath.Join(dataDir, memoryTidyConfigSubdir, memoryTidyFileName) } // LoadMemoryTidyTuning 读取 memory-tidy.json(**只读**)。 // // 文件缺失或全空白 ⇒ 零值 + nil:调用方用代码默认值(normalizeTidyConfig 会把 // 非正的上限回落默认),因此「没配过」与「配成 0」得到同一个结果。 // // 存在但解不开 ⇒ 返回错误。调用方应记一条带 source 的 WARN 并回落默认; // **不得覆盖**那个文件(损坏的文件可以人工救回,程序盖掉它就丢了人工写的意图)。 // // 它不建目录、不读老位置:新家族没有老位置(design/13 §2.1 第 5 条)。 func LoadMemoryTidyTuning(path string) (MemoryTidyTuning, error) { raw, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return MemoryTidyTuning{}, nil } return MemoryTidyTuning{}, fmt.Errorf("runtimeconfig: read %s: %w", path, err) } if strings.TrimSpace(string(raw)) == "" { return MemoryTidyTuning{}, nil } var t MemoryTidyTuning if err := json.Unmarshal(raw, &t); err != nil { return MemoryTidyTuning{}, fmt.Errorf("%w: decode %s: %w", ErrInvalidMemoryTidy, path, err) } return t, nil }