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

+9 -6
View File
@@ -172,7 +172,7 @@
| B9 | GET | `/api/v1/admin/observability/audit` | 审计链 |
| B10 | GET | `/api/v1/admin/sessions/{id}/export` | 完整会话导出 |
| B11 | POST | `/api/v1/admin/rag/query` | 知识库检索(带审计的读型 POST) |
| B12 | GET | `/api/v1/admin/config-facts` | **配置事实面(v0.68 新增,只读)**:记忆整理 7 键 + 安静时段 `QUIET_START_HOUR`/`QUIET_END_HOUR` 的**进程生效值**,逐条带 `default`/`isDefault`。**不含任何密钥/令牌值**(连「是否已配置」都不回凭据类的)。形状见 §4.10 |
| B12 | GET | `/api/v1/admin/config-facts` | **配置事实面(v0.68 新增,只读;v0.75 追加 3 键)**:记忆整理 7 键 + 后台思考心跳 3 键(`ENABLE_BACKGROUND_THINKING`/`THINK_INTERVAL_SEC`/`THINK_QUIET_INTERVAL_SEC`)+ 安静时段 `QUIET_START_HOUR`/`QUIET_END_HOUR` 的**进程生效值**,逐条带 `default`/`isDefault`。**不含任何密钥/令牌值**(连「是否已配置」都不回凭据类的)。形状见 §4.10 |
| B13 | GET | `/api/v1/admin/flow/graph` | **链路图快照 / 多轮历史回放(v0.71 新增;v0.73 增量)**,只读、不审计。`?sessionId=`(省略 = 主会话 `web`)+ **v0.73 新增 `?turnId=`**(省略 = 最近一轮,**先内存后落盘**)。响应 `{data:{sessionId,turnId,source,graph,turns[]}}`:`graph` 是**一份 §3.4.5 `graph_update` 同形快照**,没有该轮时**如实为 `null`**(**绝不画假图**);`source` ∈ `live`(内存最近一轮)/`persisted`(落盘的历史轮)/`none`;`turns[]`(`{turnId,createdAt}`,最新在前)供面板选历史轮(**v0.73**)。未接线图源 → `500 flow_unavailable`。语义见 §4.11 |
| B14 | POST | `/api/v1/admin/memories/tidy/dry-run` | **记忆整理「试算」(v0.75 新增,只读语义的 POST)**:跑同一套整理规则、把候选**直接回在响应里**,**零副作用**(不写任何表)。请求体可为空。响应 `{data:{dryRun:true,recallMode,durationMs,ruleVersion,partitions,scanned,compared,pairs,vetoed,vetoReasons,truncated?,candidates[]}}`;`candidates` 恒为非 null 数组(空数组 = 「算过了,没有建议」,与「没算出来」的错误响应**必须能区分**)。失败:未接线 → `500 tidy_unavailable`、配置关闭 → `409 tidy_disabled`、语义召回不可用 → `500 tidy_recall_unavailable`、超时 → `503 tidy_timeout`。**不审计**(只读计算)。语义与形状见 §4.12 |
@@ -716,10 +716,11 @@
| `quietHours.label` | string | 面板可直接显示的文案:`"23:00-07:00"` 或 `"已关闭"` |
| `quietHours.fallbackToDefault` | bool,**可选**(`true` 才出现) | 两端都被写成 `0` ⇒ 视为**未填**,运行时回落到默认窗口(`23:00-07:00`,此时 `startHour`/`endHour` 已是回落后的值)。⚠️ **`0/0` 不是「关闭」**:backthink 与记忆整理都会做这次回落,事实面按同一口径报,否则会把「未填」谎报成「已关闭」 |
- **固定 9 条事实**(记忆整理 7 键 + 安静时段 2 键):`MEMORY_TIDY_ENABLED`、`MEMORY_TIDY_INTERVAL_SEC`、`MEMORY_TIDY_QUIET_START_HOUR`、`MEMORY_TIDY_QUIET_END_HOUR`、`MEMORY_TIDY_RECALL_THRESHOLD`、`MEMORY_TIDY_SCAN_LIMIT`、`MEMORY_TIDY_CANDIDATE_LIMIT`、`QUIET_START_HOUR`、`QUIET_END_HOUR`。
- 🔴 **不含任何凭据**:响应里不出现 JWT 密钥 / LLM API key / 渠道 token / 数据库 DSN 的值(后端有哨兵测试钉死),**也不回凭据类键的「是否已配置」** —— 本端点只覆盖上面 9 个非敏感调参键。
- **固定 12 条事实**(记忆整理 7 键 + 后台思考心跳 3 键 + 安静时段 2 键):`MEMORY_TIDY_ENABLED`、`MEMORY_TIDY_INTERVAL_SEC`、`MEMORY_TIDY_QUIET_START_HOUR`、`MEMORY_TIDY_QUIET_END_HOUR`、`MEMORY_TIDY_RECALL_THRESHOLD`、`MEMORY_TIDY_SCAN_LIMIT`、`MEMORY_TIDY_CANDIDATE_LIMIT`、`ENABLE_BACKGROUND_THINKING`、`THINK_INTERVAL_SEC`、`THINK_QUIET_INTERVAL_SEC`、`QUIET_START_HOUR`、`QUIET_END_HOUR`。
- 🆕 **v0.75 追加(同一端点、无版本化形状变更)**:`ENABLE_BACKGROUND_THINKING` / `THINK_INTERVAL_SEC` / `THINK_QUIET_INTERVAL_SEC` 三键并入事实表(design/12 §N A4)—— 安静时段降频上线后,面板必须能读到「普通心跳周期」与「安静窗口内的心跳周期」。⚠️ `THINK_QUIET_INTERVAL_SEC` **≤0 = 关闭降频**(回落普通间隔,不是「半夜不思考」),事实面照原值回,不替用户改写。⚠️ 这几个键都是**启动期**读取的:改了要**重启**才生效(不可热改)。⚠️ `ENABLE_BACKGROUND_THINKING` 是**未接线**的死配置(全仓无消费者,见 §5「配了不生效」):它被列进事实表是为了**让面板看得见**,`value` 只是它解析出来的值,**不代表它会生效**;面板文案必须标明(见 `frontend/web/src/admin/modules/configFacts.ts`)。
- 🔴 **不含任何凭据**:响应里不出现 JWT 密钥 / LLM API key / 渠道 token / 数据库 DSN 的值(后端有哨兵测试钉死),**也不回凭据类键的「是否已配置」** —— 本端点只覆盖上面 12 个非敏感调参键。
- **「默认 / 已改」怎么读**:`isDefault:true` ⇒ 面板显示「默认」(生效值等于该键的声明默认值);`isDefault:false` ⇒ 显示「已改」。⚠️ 它**不是**「`.env` 里没写过这个键」的断言 —— 网关按红线**不读 `.env`**,无从不区分「`.env` 给的值恰好等于默认」与「完全没配」。**哪一项没配**因此不会被伪造成一个值:面板拿 `value` + `isDefault` 如实展示即可。
- 9 条事实的 `default` **必须都能取到**(后端有测试逐条钉住非空):取不到即「配置结构体 ↔ 事实表」漂移,属缺陷,**不是**「未配置」。
- 12 条事实的 `default` **必须都能取到**(后端有测试逐条钉住非空):取不到即「配置结构体 ↔ 事实表」漂移,属缺陷,**不是**「未配置」。
- **安静时段的语义(重要,别照旧实现写文案)**:安静时段**只服务「定时主动思考 → 主动搭话」**(`design/12 §6` 策略五),**不静默回复** —— 白天晚上的聊天回复都不受它约束;群聊也没有夜间静默(主人裁决 10)。因此面板文案**不得**写成「夜间全静默」。
#### (b) `GET /api/v1/admin/backthink` 条目的紧急度字段(v0.68 引入;**v0.70 起是精确档位**)
@@ -867,14 +868,14 @@
| `LLM_PROVIDER` / `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL` / `LLM_FALLBACK_MODEL` / `LLM_REASONING_KIND` 等 | 见模板 | LLM provider 组的 `.env` 合成组兜底(有有效 `models.json` 时被其覆盖) |
| `DOCUMENT_GC_INTERVAL` / `DOCUMENT_RETENTION` | `1h` / `168h` | 文档 GC(`0`/负 = 关闭 GC) |
| `LLM_USAGE_RETENTION` | `720h` | `llm_usage` 保留期(`0`/负 = 关闭清理) |
| `THINK_*` / `QUIET_*` | 见模板 | 后台思考与主动推送策略。`QUIET_START_HOUR`(`23`)/ `QUIET_END_HOUR`(`7`)是**本地钟点**的安静时段窗口(跨午夜可用;两个相等的非 0 钟点 = 关闭;两端都为 0 = 未填 ⇒ 回落默认 `23/7`):**只服务「定时主动思考 → 主动搭话」**,**不静默回复**(v0.68 起可经 B12 读到生效值) |
| `THINK_*` / `QUIET_*` | 见模板 | 后台思考与主动推送策略。`QUIET_START_HOUR`(`23`)/ `QUIET_END_HOUR`(`7`)是**本地钟点**的安静时段窗口(跨午夜可用;两个相等的非 0 钟点 = 关闭;两端都为 0 = 未填 ⇒ 回落默认 `23/7`):**只服务「定时主动思考 → 主动搭话」**,**不静默回复**(v0.68 起可经 B12 读到生效值)。`THINK_INTERVAL_SEC`(`900`)/ **`THINK_QUIET_INTERVAL_SEC`(`3600`,v0.75 新增)**:落在安静窗口内时,**周期性心跳**改用后者降频、窗外回落前者;**`≤0` = 关闭降频**(回落普通间隔,**不是**「半夜不思考」)。⚠️ 它**只降周期性心跳**:事件驱动(安全域/急性事件)照旧立刻触发、照旧穿透安静时段 |
| `MEMORY_TIDY_ENABLED` | `true` | 记忆整理总开关(`design/23 §4`)。关闭 ⇒ 完全不跑(日志写明);整理**只产建议、绝不自动改记忆**。**v0.75 起**:关闭时试算端点(B14)也**拒绝作答**(`409 tidy_disabled`),不绕过这个选择 |
| `MEMORY_TIDY_INTERVAL_SEC` | `86400` | 整理周期(秒),是**下限**:实际在低峰窗口起点跑 ⇒ 真实间隔取「窗口每天一次」与本值的较长者 |
| `MEMORY_TIDY_QUIET_START_HOUR` / `MEMORY_TIDY_QUIET_END_HOUR` | `23` / `7` | 记忆整理的**低峰窗口**(本地钟点,默认与 backthink 同值);两端相等且非 0 = 关闭窗口、改为纯周期;两端都为 0 = **未填**,运行时回落到默认 `23/7`。⚠️ 窗口**只管心跳调度**:试算端点(B14)**不受它约束**(否则「现在就想看」正是它存在的理由) |
| `MEMORY_TIDY_RECALL_THRESHOLD` | `0.80` | 候选**召回门**(余弦),**不是**合并判据(可否合并由包含关系 + 规则否决决定)。试算端点(B14)用**同一个**值 |
| `MEMORY_TIDY_SCAN_LIMIT` / `MEMORY_TIDY_CANDIDATE_LIMIT` | `5000` / `20` | 每分区每轮的扫描上限 / 候选上限。试算端点(B14)用**同一对**上限(不另造一套) |
> **上面 9 个键(记忆整理 7 + 安静时段 2)自 v0.68 起可经 `GET /api/v1/admin/config-facts` 读到**「进程生效值 + 声明默认值」;在此之前面板没有任何 env 读取面。
> **上面 12 个键(记忆整理 7 + 后台思考心跳 3 + 安静时段 2)自 v0.68 起可经 `GET /api/v1/admin/config-facts` 读到**「进程生效值 + 声明默认值」(3 个心跳键自 v0.75 起并入);在此之前面板没有任何 env 读取面。
| `SUBAGENT_TIMEOUT_SEC` | `600` | 单个子代理整次运行时限 |
| `MCP_SERVERS_JSON` | 空 | MCP server 注册表(JSON 数组)。⚠️ 扁平 `MCP_NAME`/`MCP_COMMAND`/… 为**未接线残留键,配了不生效** |
| `CHANNEL_*` / `CHANNELS_JSON` | 见模板 | 渠道接入(`.env` 为兜底,面板 `channels.json` 优先) |
@@ -887,6 +888,7 @@
**与契约相关但「配了不生效」的键**:
- **`CHAT_MAX_PARALLEL_TURNS`:未接线**。编排器有构造期旋钮 `Deps.MaxParallelTurns`,但**组合根未从任何 env 读它** ⇒ **生产恒为默认 2**(`.env` 里配了不生效)。**本文档不把它登记为可用旋钮**。
- **`ENABLE_BACKGROUND_THINKING` / `THINK_SILENCE_TIMEOUT_SEC`:未接线**(全仓无消费者;`DevDocs/audit/07-backthink-automation-iot.md` §1-17,是否接线待主人拍板)。`ENABLE_BACKGROUND_THINKING` 自 v0.75 起被列进 B12 的只读事实面 —— 那是为了**让面板看得见这个死开关**,**不是**「它已生效」的声称:`facts[].value` 只是解析值,配 `false` 也不会关掉后台思考。`THINK_SILENCE_TIMEOUT_SEC` **未列入**事实面。
- 旧 `MCP_*` 扁平键:生产装配只读 `MCP_SERVERS_JSON`。
- `GETUI_*`:SDK 未接入,预留占位、当前不生效。
@@ -902,6 +904,7 @@
**记忆整理「试算」端点 —— 新增 `POST /api/v1/admin/memories/tidy/dry-run`:跑同一套整理规则、把候选直接回在响应里,且一行都不写**(依据主人 2026-10-05 要求「现在就想看到在 736 条上它会建议合并哪些,而不等到 23:00」;对齐 `design/23 §4`「低峰定期后台任务 + 手动可触发的只读试算」)。
- **纯增量**:新增 1 条 `writeAuth`(owner/admin)管理的**只读语义 POST**(§2.2 B14 / §4.12)。既有端点、既有帧、既有 env 键语义**一字未改**。
- **同版追加(无形状变更)**:`GET /api/v1/admin/config-facts` 的事实表由 9 条扩到 **12 条** —— 并入 `ENABLE_BACKGROUND_THINKING` / `THINK_INTERVAL_SEC` / **`THINK_QUIET_INTERVAL_SEC`(安静窗口内心跳周期,默认 `3600`;`≤0` = 关闭降频)**,支撑 design/12 §N「静默时段降频」(§2.2 B12 / §4.10(a))。`facts[]` 形状与 `quietHours` **一字未改**,只是多了 3 个元素 ⇒ 老客户端零影响。
- **零副作用是硬约束**:端点走的是与低峰定时轮**共用**的规则路径(同一召回门 + 同一包含判定 + 同一规则否决 + 同一每分区候选上限),但**候选不落库** —— 不写 `memory_tidy_candidates`、不写 `memory_tidy_runs`、**绝不改 `memory_items` 的任何列**。后端有「调用前后三张表逐行快照比对」的单测钉死(另有一条断言「试算候选 == 真跑一轮落库的候选」)。
- **失败绝不伪装成「没有建议」**:未接线 → `500 tidy_unavailable`、`MEMORY_TIDY_ENABLED=false` → `409 tidy_disabled`、语义召回(embedding)不可用 → `500 tidy_recall_unavailable`、超时 → `503 tidy_timeout`、其余内部失败 → `500 tidy_dry_run_failed`。**不回** `candidates:[]` 来表示失败 —— 空数组唯一含义是「算过了,这一轮没有建议」。
- **试算专属的严格**:语义召回不可用时,**试算报错而不静默降级**到词项余弦(否则面板会把「语义路径没跑起来」读成「没有候选」)。低峰定时轮与手动轮的降级口径**不变**。