feat(backthink): 静默时段心跳降频(A)—— THINK_QUIET_INTERVAL_SEC 默认 3600

主人裁定「先做 A(静默降频)再转向 C」;背景实测:静默期**思考频率不变**(恒定 900s 心跳),只拦打扰 ⇒ 成本照付。
- 新键 THINK_QUIET_INTERVAL_SEC(默认 3600):窗口内用该间隔、窗外仍用 THINK_INTERVAL_SEC;**≤0 = 关闭降频**(不许出现「半夜完全不思考」暗坑);窗口关闭/钟点非法 ⇒ 不降频
- 心跳**每拍按时刻选间隔**(放到循环外就永远不生效——变异实测能打红)
- 🔴 不变量:**事件驱动不被降频影响**(TestBackthink_EventTrigger_NotSlowedByQuietInterval 钉住)
- 事实卡 9→12 条(+思考总开关/普通间隔/静默间隔,标「改需重启」);契约形状未变、未升版
- ⚠️ 如实登记:ENABLE_BACKGROUND_THINKING 是**死配置**(全仓无消费者),已在 env/面板标签/契约三处标注,**是否接线待主人拍**
- 变异 5 条全红;C 的缺口清单(唤醒面 7 处)已回给主会话排期
This commit is contained in:
AskaEth committed 2026-10-05 22:38:17 +08:00
1 parent d6ed9cfa2b
commit 236b202fda
13 files changed
+633 -54

No files matched your search

@@ -8,8 +8,8 @@ import (
)
// 本文件承载 P3「配置只读事实面」:把面板此前**看不见、只能照模板猜**的一组调参
// 键做成只读事实展示(记忆整理 7 键 + 安静时段 2 键)。三条边界(AGENTS §10 与
// 主人红线):
// 键做成只读事实展示(记忆整理 7 键 + 安静时段 2 键 + 后台思考心跳 3 键)。三条边界
// (AGENTS §10 与主人红线):
//
// 1. **键名单的权威来源是 `.env.example`**(不是 `.env`、也不是本文件自己发明
// 一份清单):事实表逐条与模板键名对齐,由
@@ -20,7 +20,7 @@ import (
// 面板看到的是运行实例真正在用的数,而不是 `.env` 原文。每条事实另带**声明
// 默认值**(从 `default:"…"` 标签反射读出)与 `isDefault`,于是「这项还是默认
// 值」与「这项被改过」都能如实显示,不必假装知道 `.env` 里写过什么。
// 3. **绝不回任何密钥 / 令牌值**:本端点只覆盖上述 9 个非敏感调参键,响应里连
// 3. **绝不回任何密钥 / 令牌值**:本端点只覆盖上述 12 个非敏感调参键,响应里连
// 凭据类键的「是否已配置」都不出现。
// `TestAdminConfigFacts_NeverLeaksSecretValues` 用哨兵值把这条钉死。
//
@@ -77,7 +77,7 @@ type configFactSpec struct {
}
// configFactSpecs 是事实表的**唯一真源**:顺序即面板显示顺序(先记忆整理,
// 后安静时段)。键名必须与 `.env.example` 逐字一致(测试钉住)。
// 后安静时段与后台思考心跳)。键名必须与 `.env.example` 逐字一致(测试钉住)。
func configFactSpecs() []configFactSpec {
return []configFactSpec{
{"MEMORY_TIDY_ENABLED", func(c *config.Config) string {
@@ -101,6 +101,17 @@ func configFactSpecs() []configFactSpec {
{"MEMORY_TIDY_CANDIDATE_LIMIT", func(c *config.Config) string {
return strconv.Itoa(c.MemoryTidy.CandidateLimit)
}},
// 后台思考心跳(design/12 §N A4):安静窗口只降低周期性心跳的频率,
// 而 env 是**启动期**读的 ⇒ 面板照实列出「生效值 + 声明默认」并写明改需重启。
{"ENABLE_BACKGROUND_THINKING", func(c *config.Config) string {
return strconv.FormatBool(c.Background.Enabled)
}},
{"THINK_INTERVAL_SEC", func(c *config.Config) string {
return strconv.Itoa(c.Background.IntervalSec)
}},
{"THINK_QUIET_INTERVAL_SEC", func(c *config.Config) string {
return strconv.Itoa(c.Background.QuietIntervalSec)
}},
{"QUIET_START_HOUR", func(c *config.Config) string {
return strconv.Itoa(c.Background.QuietStartHour)
}},
@@ -117,7 +128,7 @@ func formatRatio(v float64) string {
}
// buildConfigFacts 组装配置事实面。它是纯函数(不读文件、不读 env),因此可
// 直接单测;cfg 为 nil 时回退到一个零值配置(此时 9 条事实全部照实报零值 +
// 直接单测;cfg 为 nil 时回退到一个零值配置(此时 12 条事实全部照实报零值 +
// 声明默认值,面板会看到 isDefault=false 的「已改」而不是一个编出来的值)。
func buildConfigFacts(cfg *config.Config) AdminConfigFacts {
if cfg == nil {
@@ -106,6 +106,35 @@ func TestAdminConfigFacts_QuietHoursAreProcessEffective(t *testing.T) {
}
}
// TestAdminConfigFacts_BackthinkHeartbeatKeys 钉住 design/12 §N A4 新增的三条
// 后台思考心跳事实(`ENABLE_BACKGROUND_THINKING` / `THINK_INTERVAL_SEC` /
// `THINK_QUIET_INTERVAL_SEC`):值取**进程生效值**、总条数确实是 12。键名与
// `.env.example` 逐字一致那一半由 TestAdminConfigFacts_KeysMatchEnvExample 钉。
func TestAdminConfigFacts_BackthinkHeartbeatKeys(t *testing.T) {
t.Parallel()
cfg := cfgDefault()
cfg.Background.Enabled = false
cfg.Background.IntervalSec = 7200
cfg.Background.QuietIntervalSec = 0 // ≤0 = 关闭降频;事实面必须如实报 0
_, _, facts := getConfigFacts(t, cfg, "")
if len(facts.Facts) != 12 {
t.Fatalf("facts = %d, want 12(记忆整理 7 + 后台思考心跳 3 + 安静时段 2)", len(facts.Facts))
}
for key, want := range map[string]string{
"ENABLE_BACKGROUND_THINKING": "false",
"THINK_INTERVAL_SEC": "7200",
"THINK_QUIET_INTERVAL_SEC": "0",
} {
got := factByKey(t, facts.Facts, key)
if got.Value != want {
t.Fatalf("%s value = %q, want %q(进程生效值)", key, got.Value, want)
}
if got.IsDefault {
t.Fatalf("%s 被改过,isDefault 必须为 false:%+v", key, got)
}
}
}
// TestAdminConfigFacts_DeclaredDefaultsAndDefaultFlag 是判据 ②:每条事实都带
// **声明默认值**(default 标签,非空),且当生效值等于它时 isDefault=true ——
// 面板因此能如实显示「默认」,而不是把默认值当成「用户配过的值」。
@@ -119,6 +148,9 @@ func TestAdminConfigFacts_DeclaredDefaultsAndDefaultFlag(t *testing.T) {
"MEMORY_TIDY_RECALL_THRESHOLD": "0.8",
"MEMORY_TIDY_SCAN_LIMIT": "5000",
"MEMORY_TIDY_CANDIDATE_LIMIT": "20",
"ENABLE_BACKGROUND_THINKING": "true",
"THINK_INTERVAL_SEC": "900",
"THINK_QUIET_INTERVAL_SEC": "3600",
"QUIET_START_HOUR": "23",
"QUIET_END_HOUR": "7",
}
@@ -127,7 +159,10 @@ func TestAdminConfigFacts_DeclaredDefaultsAndDefaultFlag(t *testing.T) {
Enabled: true, IntervalSec: 86400, QuietStartHour: 23, QuietEndHour: 7,
RecallThreshold: 0.8, ScanLimit: 5000, CandidateLimit: 20,
},
Background: config.BackgroundConfig{QuietStartHour: 23, QuietEndHour: 7},
Background: config.BackgroundConfig{
Enabled: true, IntervalSec: 900, QuietIntervalSec: 3600,
QuietStartHour: 23, QuietEndHour: 7,
},
}
status, _, facts := getConfigFacts(t, defaultCfg, "")
if status != http.StatusOK {
@@ -274,7 +309,7 @@ func TestAdminConfigFacts_NeverLeaksSecretValues(t *testing.T) {
}
// TestAdminConfigFacts_KeysMatchEnvExample 是判据 ④:键名单的权威来源是
// `.env.example` —— 9 个键必须逐字出现在模板里(键名写错一个字母,用户照面板
// `.env.example` —— 12 个键必须逐字出现在模板里(键名写错一个字母,用户照面板
// 抄去改就是静默不生效,AGENTS §10 的 E6 固定动作)。
func TestAdminConfigFacts_KeysMatchEnvExample(t *testing.T) {
t.Parallel()