,"reason":"<原因码/文案>"}`。`applied=true` = 这次保存**真的触发了热重载**(按落盘后的配置重建工具 + 原子替换注册表条目 ⇒ **下一轮生效、不需重启**);`applied=false` 且保存前**本来没有**可用工具 ⇒ 文件留下、如实报 `false` + `reason`(那种情形没有能力可丢,报错反而会让主人**没法先配好**) |
+| `plugin.shootPage` | 拍一张**当前页面** | write(table `rowActions`) | `{"row":{…}}`(载荷不被解释,只为触发) | `{"shot":{…}}`,`shot` = `browser` 工具 `screenshot` 的返回值原样(见下面「模型面」一节),含 `path` / `bytes` / `sha256` / `format` / `width` / `height` / `dimensionsKnown` / `degraded` / `turnAttachment` / `hint` |
+| `plugin.checkFingerprint` | 重新做一次指纹自检(**会启动浏览器**) | write(table `rowActions`) | `{"row":{…}}` | `{"rows":[…]}`(与 `plugin.getFingerprint` 同形,刷新后的读数) |
+
+**字段名(`values` 的键,逐字与 `$DATA_DIR/config/plugins/browser.json` 的 JSON tag 一致)**:`headless`(bool)、`executable`(string,留空 = 自动探测本机 Chrome/Chromium)、`cdpEndpoint`(string,显式给出时取代自动探测)、`profileDir`(string)、`viewportWidth` / `viewportHeight`(number,声明面已给 min/max)、`idleTimeoutSeconds`(number)、`concurrency`(number,**声明面 min = max = 1**:同一 profile 起两个实例会互锁)。🔴 **未知字段一律拒(`400 invalid_payload`),绝不静默忽略** —— 静默忽略会让「我改了但它没生效」变成查不出的谜,而且不该在盘上留下半份改动。
+
+**错误码(本插件在 (b) 通用表之上追加的,宿主只透传「码 + 文案 + HTTP 状态」)**:
+
+| 状态 | `error.code` | 触发条件 | 客户端应做什么 |
+|---|---|---|---|
+| `400` | `invalid_payload` | 载荷不是对象 / 缺 `values` / `values` 不是对象 / **未知字段** / 字段类型错 | 显示文案原文;**不要**当成「保存成功」 |
+| `409` | `browser_unavailable` | 本机**没有**可用浏览器来源(未探测到 Chrome/Chromium,也没有在跑的 CDP 端点)⇒ `plugin.shootPage` / `plugin.checkFingerprint` 做不了 | 提示「先去配置里填可执行文件或 CDP 端点并保存」;面板可以配 ⇒ 不是死路 |
+| `409` | `browser_session_not_open` | `plugin.shootPage` 时浏览器**没有打开的页面** | 提示「先让她打开一个页面(或在有头窗口里人工接管)」—— **面板刻意不替模型开页**(否则它就成了「任意 URL 的截图器」) |
+| `409` | `config_rejected` | 这次保存**没能生效**(新配置会让工具不可用),**已按字节回滚**到保存前那一份 | 显示原因原文;**盘上没有变**,不必再手动改回 |
+| `500` | `config_unreadable` | 面板配置存在但**读不出来**(损坏 / 权限)⇒ 本次**没有**改动它 | 显示「配置读不出来」,指向服务端日志;**不要**重试保存 |
+| `500` | `config_write_failed` | 配置写不进去(磁盘 / 权限);⚠️ **回滚也失败**时也用这个码,文案明说「盘上的文件可能已经变了」 | **不假装改动没发生**;提示可能需要人工检查盘上文件 |
+| `502` | `browser_action_failed` | 浏览器动作本身执行失败(截图 / 自检) | 详细原因只进后端日志;显示「执行失败」+ 文案 |
+
+> **`applied:false` + `reason` 是正常结论,不是错误**:保存前**本来就没有**可用工具时(例如第一次配一个还没起来的容器端点),文件留下、`200` + `applied:false`。客户端**不要**把 `applied:false` 渲染成失败 —— 要说的是「已保存,但这次没有东西可换/没生效」+ `reason`。
+
+**模型面(非客户端契约面,登记供跨端对照)**:`browser` 工具的 `screenshot` 返回值新增 **`turnAttachment`**(**闭集**,v0.82):
+
+| 取值 | 含义 |
+|---|---|
+| `queued` | 截图已入**本会话的待送附件箱**,将在本回合的下一次模型调用里作为附件送出(主模型收不了图时以视觉/OCR 的文字形态) |
+| `no-session` | 本次调用拿不到会话 id(子代理 / 未接编排器的调用),**未入箱** ⇒ 由模型自己按 `path` 去读 |
+| `outbox-full` | 该会话的箱子已满(**每会话最多 4 张**、TTL 10 分钟),**丢掉了最旧的那张**(如实报,不静默) |
+| `disabled` | 本工具没有附件箱(`New(nil)` 这类夹具),未入箱 |
+
+配套的 `hint` 字段是一句**如实**的下一步提示(`queued` 时说「已经作为图片附件送出」;其余档说「图没进上下文,按路径读」)。**「入箱」到「进上下文」靠 `loop.Options.TransformContext` 上挂的钩子**(`cmd/cyrene/wire_browser_context.go`)—— 它**取走即消费**(同一张图不会被送两次),也因此**不会**进 checkpoint、不会进后台记忆提取的输入面。⚠️ **该挂点在 v0.82 登记时仍在工作树、未提交**;缺了它,`turnAttachment` 仍会照报 `queued` 而图**一张都不会进上下文**(**零症状**的坏法,因此有一条源码级用例盯着 `wire_runtime.go` 那一行)。
+
+---
+
+### 4.15 记忆整理「应用面」(B20–B24;**v0.83 新增,切片 TIDY-1**)
+
+> 依据 `design/23`(§7 人工在环 / §8 软动作与回滚 / §10 status 列 / §11 管理面)。**背景**:v0.75 起只有「试算」(B14/B15 —— 只回建议、**零副作用**)⇒ 主人的实测口径是「候选 77 条**全 pending**、两天**一条没应用**」。本版把**执行面**补上:候选能列、能应用、能回滚,整理能手动真跑。
+
+#### (a) 端点族一览
+
+| 端点 | 语义 | 关键参数 | 审计 |
+|---|---|---|---|
+| B20 `GET …/memories/candidates` | 列候选(含 `id`/`runId`/证据) | `status`(省略 = 全部)、`limit`(缺省 100 / 钳 500 / 非法 400) | 无(只读) |
+| B21 `POST …/candidates/{id}/apply` | 应用**一条** | 无体 | `memory.tidy.apply` |
+| B22 `POST …/candidates/apply` | **批量**应用 | `runId`(省略 = **全部待定**)、`limit`(缺省 100 / 钳 500 / 非法 400) | `memory.tidy.apply` |
+| B23 `POST …/candidates/{id}/revert` | 回滚**一条**已应用 | 无体 | `memory.tidy.revert` |
+| B24 `POST …/memories/tidy/run` | 手动**真跑**一轮(写候选 + 留痕,**不自动应用**) | 无体 | 领域侧记 `memory.tidy.run`(一次点击一条) |
+
+形状逐字段见 §2.2 的 B20–B24 行。**门一律 `writeAuth`**(owner\|admin)—— 候选正文含隐私,与同族 `GET /admin/memories`、`POST …/tidy/dry-run` 同档。⚠️ `design/23 §11` 表格把**回滚**标为 owner-only,而实现用了 `writeAuth`:**口径差异待裁决**(若以设计为准,`routes.go` 里把那两行换成 `ownerAuth` 即可,一行一处;与 §2.2 B17 的同类差异是同一件事)。
+
+#### (b) 三条硬约束(客户端与面板都按这个编程)
+
+1. **幂等不是错误**:执行闸是**候选状态的原子条件更新**(`WHERE id=? AND status='pending'` + `RowsAffected`),记忆行侧还有第二道闸(`AND status='active'`)。因此并发重复点击**只有一个赢家**,其余拿到 `200` + `status:"skipped"` —— 面板**不得**把它渲染成失败,也**不得**据此提示「重试」。
+2. **超限必须如实**:批量回执的 `total` 与 `applied+skipped+failed` **口径不同**(前者 = 命中筛选的**真**待定数,后者只覆盖 `limit` 之内),差集由 **`truncated:true`** 表达。🔴 这是「不许把本轮 100 / 共 342 画成全部完成」的契约级要求。
+3. **软动作、永不物理删除**:merge = 被吸收行 `status='merged'` + `merged_into`;archive = `status='archived'`;**`content` 一个字不改、行一行不删** ⇒ `revert`(B23)必然无损可逆。同时,**软删行必须真的从读侧消失**(v0.83 行为修复):记忆的四条列表读路径(`ListByPersona` / `List` / `ListScope` / `ListProject`)只回 `status='active'`,因此被软合并/归档的记忆**不再被召回、不再进上下文、不再出现在 `GET /admin/memories`**。🔴 **只有两处刻意不过闸**:按 id 的 `Get`(撤销软合并必须先读得到被吸收的行)与 **`GET /admin/memories/reviews`**(审查留痕本身没有 status 概念,且它是软删行的查看入口 —— 加闸等于审计自我抹除)。
+
+#### (c) 调度:到点即跑 + catch-up(对客户端零形状影响)
+
+原实现是「`interval=24h` 的 ticker 从**进程启动**算起」⇒ 后端两天重启十几次,**24h 永远走不完**(`memory_tidy_runs` 只有 1 行,这就是「两天没跑」的一半根因)。现在以 **`memory_tidy_runs` 的留痕行**作为「上次到底跑过没有」的**持久事实**,启动时与心跳到点都判一次(三条判据),**重启不饿死**。B24 是它的手动入口(panic button),与心跳**同一套规则、同一份配置**。
+
+#### (d) 诚实边界(**别当已验证**)
+
+- 🔴 **生产库全程零动作**:1191 行记忆与 77 条待定候选**都没有被应用或清理**。本版交付的是**能力**,不是「已经清过了」—— 真跑与批量应用要主人拍板后执行(可分批、可 revert)。
+- **面板上的真机端到端未跑**:本版登记时**前端操作面还没落地**(前端切片并行在做),因此本版证据是**存储层单测 + 网关端到端测试 + 变异自证**,不含「面板点一下真生效」。
+- **旧候选的 `run_id` 是空串**(77 条在生产库里都是这样)⇒ 「**按 run 批量**」(B22 带 `runId`)**找不到它们**;要清它们必须走**全部待定**那一档。
+- 候选表的 `run_id` 列目前**只在存储包前滚**(`memory.NewStore` 的 `ensureTidySchema`),**未**同步进迁移 `0019`;组合根每次启动都会跑到前滚,因此生产库拿到该列没问题,但「迁移是不是唯一真源」这条口径**有欠**。
+- **`memory.recall` 的语义召回在本机不可复现**(远端 embedder 对同一文本返回不同向量,TIDY-2 诊断发现)—— 它**不影响**本族的规则层判定与状态机,但**影响「整理候选是否可复现」**,登记在此以免被当成实现缺陷。
+
+
+
+#### (e) v0.85:「不再只产建议」+ `link` 停产 + 按动作批量拒绝(B25)
+
+**① 轮末自动落地(默认生效)**:整理跑完一轮后会**自动**把两类**规则层已判定为高置信**的候选落地,不再等人点:
+- `merge`:**字面重复**(两侧正文逐字相同)**或**「事实集合等价 ∧ 相似度 ≥ **0.95**」;
+- `archive`:理由 = `one_off_event` **且不像习惯**(`design/23 §5.2-1` 红线:习惯类会因正文里带钟点被误判成一次性事件,**归档它是错的**)。
+
+🔴 **不自动**(别指望它替你干这些):`reclassify`、**低置信** merge、跨分区 / 跨角色、**任何改正文**、**受限来源(群聊 / 陌生人)**。
+**六条护栏**:软动作(只改 `status` / `merged_*`,**正文不改、行不删**)· **每轮上限 50**(硬顶 500,超限**如实** `truncated` + 真总数)· 审计 `memory.tidy.apply`(`auto=true`,**不带正文**)· **可 revert**(回滚路径与人工应用**同一条**)· 幂等(候选状态条件更新)· **一键关回只产建议**:写 `$DATA_DIR/config/memory-tidy.json` → `{"autoApply": false}`(调上限同文件 `autoApplyPerRound`;**启动期读一次,改了要重启**;**零新 env 键**)。
+
+**② `link` 候选已停产**:`link` 在 `design/23 §14` 里是**未细化项**(既无存储落点、也无数据模型),而 `applyable()` 只认 `merge/archive/reclassify` ⇒ **应用一条 `link` 只会拿到 `skipped`**。⇒ 规则层不再产出它(判 `keep`)。`TidyLink` 常量保留**只为读历史行**与「不可应用」判定。
+
+**③ 存量清理走 B25**(幂等、只改候选状态、不碰记忆):
+
+```bash
+BASE=http://127.0.0.1:10942
+# ① 先看现状(默认只看 pending;limit 缺省 100、钳 500)
+curl -s -H "X-API-Key: $KEY" "$BASE/api/v1/admin/memories/candidates?status=pending&limit=500"
+
+# ② 一次性拒绝全部 link 候选(幂等;limit 缺省 1000、硬顶 10000)
+curl -s -X POST -H "X-API-Key: $KEY" \
+ "$BASE/api/v1/admin/memories/candidates/reject?action=link&limit=1000"
+# 期望 {"data":{"action":"link","limit":1000,"total":501,"rejected":501,"truncated":false}}
+
+# ③ 复核:同一调用再来一次 ⇒ total:0 / rejected:0(**不是错误**)
+```
+
+🔴 **只该整类拒 `link`(必要时 `keep`)** —— `merge/archive/reclassify` **有执行语义**,交给自动档或人工,别整类拒掉。
+⚠️ 本条的 `total/rejected/truncated` 口径与 B22(批量应用)**同构**:`total` 是真命中数,处理数只覆盖 `limit` 之内。
+
+
+
+### 4.16 助手文本的段落标签约束(**v0.84 新增,产出侧硬约束**)
+
+**约束(一句话)**:助手文本在**落库、对外读接口、IM 镜像**里**不得出现未配对的段落标签** —— 不许有落单的 `` 或 `
`。语义上:**一个 `` = 一段,段落不嵌套**。
+
+**为什么写进契约**(这曾是真实纠纷):安卓端从 `GET /api/v1/sessions/web/messages` 读到过一条 `content` 形如
+
+```
+
满意地点点头
+真乖♪ 这可是今天最让人家放心的一句。
+那现在——想让人家陪你看会儿晚霞,还是安静坐着发会儿呆?
+```
+
+他们的解析契约 `P_BLOCK_RE = ]*)?>([\s\S]*?)
` 是**非贪婪**的 ⇒ 第三段必然匹配成「外层 `` 开、内容 = `
正文`、`
` 收」⇒ **用户直接看到字面的 ``**。这不是他们解析器的口味问题:**任何按该契约定实现的消费方(Web 面板 / IM 桥 / 第三方)都会踩**。
+
+**实测根因(两半都在产出侧,已修 `8051906`)**:
+
+1. 规整器**原样放过**该形状(分块正则非贪婪 ⇒ `
X
` 被当成「一个已完整包裹的 `` 块」逐字保留;更糟的是旧的覆盖检查用**同一非贪婪正则**,护栏本身是盲的);
+2. 规整器还会**亲手制造**它:段首 `
` 未闭合(模型末段漏 `
`)时**无条件再套一层** —— 实证:模型吐 `那现在…?`(无闭标签)经旧代码规整后与上报串**逐字节相同**;
+3. 顺带修掉一个**幂等漏洞**:落单 `
` 旧行为 ⇒ `正文。
`,再跑一次又变 ⇒ 越跑越多。
+
+**修后的处置口径(产出侧,逐条有测试)**:`X
` ⇒ `X
`;`X`(漏闭)⇒ 补 `
`(**补闭不补开**);`正文
` ⇒ `
正文
`(合并不吞字);`A
` ⇒ `
A
`(丢空开标签、不造空气泡);`` ⇒ 整对丢;落单 `` ⇒ 丢标签、**正文不动**。
+
+**🔴 客户端要做 / 不用做的**:
+
+- **不用做**:不必为这个畸形写特例解析器(产出侧已保证不再产出)。
+- **仍要做**:**保留**你现有的「剥落单标签」容错 —— ① **历史行不回填**(库里已经有畸形的行);② 产出链**不止一条**(渠道镜像 / 主动推送 / 后台思考各有包装点),不能假设全都干净。
+- **护栏(不许退化)**:`1 < 2`、`a`、``)、代码围栏内的 `
`、`` 块内的 ``、自闭合 `
` —— 这些**都不是**段落标签,一律原样保留(产出侧与消费侧**同一条口径**)。
+
+**一个保守处置(待设计裁决,生产库 0 例)**:`A\n\n
B
`(未闭合段落之间夹正文)产出侧选择**合并** ⇒ `A\n\nB
`,**不**做「插入式隐式收尾」(那要猜作者意图)。客户端不必为此适配。
+
+## 5. env 与模型配置(与契约相关)
+
+> **`.env` vs 面板文件**:面板「LLM 模型」页保存写 `/models.json`(默认 `./data/models.json`);「渠道管理」页写 `/channels.json`。**只要文件存在且有效,运行时优先用它**,`.env` 只作兜底/播种层。`.env.example` 与实现同步(键名逐字一致、值占位符)。
+
+| 键 | 默认 | 与契约的关系 |
+|---|---|---|
+| `PORT` | `8080` | 网关唯一端口(含 REST/WS/admin) |
+| `HOST` | `0.0.0.0` | 监听地址 |
+| `ADMIN_PATH` | `admin` | 管理面板**前端路由前缀**(`/`+值)。**不是 REST 契约**:`/api/v1/admin/*` 一律不藏。非法值回退 `/admin`;路径形如面板入口但前缀不符 → `404` |
+| `CHAT_IMAGE_MAX_BYTES` | `5242880`(5 MiB) | `chat.image` **字符串长度**上限的换算源(**单位是字节**,按 base64 4/3 膨胀换成字符:`ceil(n/3)*4 + 64`,默认 **6990572 字符**;只作用于 **base64 / `data:` 形态**,URL 形态不走这道检查)。`≤0` = 用默认;**只能收紧**,钳到 vision 硬顶(见 §4.5) |
+| `CHAT_DOCUMENT_MAX_BYTES` | `20971520`(20 MiB) | `POST /documents` 单文件上限(`≤0` 用默认;与图片上限**刻意解耦**) |
+| `AUTH_REFRESH_TTL` | `720h` | refresh token 有效期(access 固定 24h) |
+| `AUTH_REFRESH_ROTATE_DISABLED` | `false` | `true` = 关闭 refresh 单次轮换 |
+| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | 占位 | 登录凭据(`POST /auth/login`);必填 |
+| `JWT_SECRET` | 占位 | 必填、≥32 字符(签名 JWT) |
+| `DATA_DIR` | `./data` | 数据目录;接受保留字 `platform`(大小写不敏感)→ 平台标准目录 |
+| `DB_SQLITE_PATH` | `./data/cyrene.db` | SQLite 路径 |
+| `DB_PG_URL` | 空 | pgvector **可选增强**;配置了必须真实走语义/向量路径。是否生效另由 `GET /admin/system/storage` 的 `vector.{enabled,ready}` 如实报告 |
+| `ALLOWED_ORIGINS` | 示例值 | CORS + WS Origin 白名单(同 host 与回环别名额外放行) |
+| `WS_MAX_CONNECTIONS` | `1000` | WS 连接上限(配置项,**未确证**是否在网关强制生效 —— 代码只见解析,未见到接线点) |
+| `LLM_MULTIMODAL` | `false` | **主模型自身能否收图**的 `.env` 等价物(只作用于 `.env` 合成组;有有效 `models.json` 时以该组 `multimodal` 为准)。`true` = 图片直进主模型;`false`/留空 = 图片交 vision/OCR 工具。**非法值启动即报错** |
+| `LLM_VISION_MODEL` / `LLM_OCR_MODEL` | 空 | **只供视觉/OCR 工具**调用;**不代表主模型能收图** |
+| `LLM_CONTEXT_WINDOW` | 空 → 131072 | 上下文窗口(预算推导依据);负数启动报错 |
+| `CONTEXT_MAX_TOKENS` | 空 → 按窗口推导 | `>0` 显式覆盖;`0`/负 = 关闭压缩。⚠️ 若 `.env` 里还留着 `32768`,它就是显式覆盖、推导不生效 |
+| `CONTEXT_BUDGET_RATIO` | `2/3` | 预算占窗口比例(接受 `2/3` 或 `0.6667`) |
+| `DOCUMENT_MAX_INJECT_CHARS` | 空 → 预算 1/8 | 单份附件注入上限 |
+| `GUARDRAIL_MAX_INPUT_CHARS` / `GUARDRAIL_KEYWORDS` | 空 | 请求侧字符上限 / 关键词护栏 |
+| `VOICE_STT_TIMEOUT_SEC` | `20` | **只约束 IM 渠道入站语音转写**;Web 客户端 `voice_input` 不受它约束 |
+| `VOICE_TTS_VOICE_ID` / `VOICE_TTS_FORMAT` / `VOICE_TTS_MODEL` | 空 | `POST /audio/speech` 当前遵循的服务端配置(请求体 `voice`/`format` 为前向兼容接受但暂不生效) |
+| `VOICE_TTS_INSTRUCTION` | 空 | 系统音色 `longxiaochun_v3` 固定格式、**不使用自由 instruction** |
+| `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_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` / **`200`** | 每分区每轮的扫描上限 / **候选**上限(⚠️ 两者都是**每分区**的量,不是全库)。候选上限的默认值 **v0.77 由 `20` 抬到 `200`**(首轮真跑四个分区同时打满旧值 ⇒ 候选数由上限而不是规则决定);命中它时响应 `truncated` 带 `<分区>(candidates)`。试算端点(B14)用**同一对**上限(不另造一套) |
+| `MEMORY_TIDY_COMPARE_LIMIT` | `2000` | **v0.77 新增**:每分区每轮**真正参与两两比对**的条目上限(性能护栏,两两相似度是 O(n²·dim))。此前**写死为 `400` 且没有 env 键**,实测把 459 条的分区截到 400 ⇒ `compared(705) < scanned(784)`(观测口径被上限决定)⇒ v0.77 抬到 `2000` 并开放为 env。命中它时响应 `truncated` 带 `<分区>(compare)` + 日志 WARN。`≤0` = 用默认 |
+| `MEMORY_REVIEW_ENABLED` | `true` | **v0.77 登记**:记忆「入库前审查」(`design/23 §15.1 D3`,切片 MT-B)总开关。新记忆落库后由**独立的记忆管理会话**(复用 `sess-memorytidy` 装配)在有界后台队列里判定 `skip`/`merge`/`modify`/`create`,结论以**软合并**回写(`status` + `merged_into`/`merged_from`/`merge_count`,**可 revert、绝不改既有行 `content`**)。`false` ⇒ 组合根不建、不接,写入路径**逐字回到**改动之前。默认开启是因为判定是**纯规则、零模型调用**(T1 灰区模型判定**未实现**) |
+| `MEMORY_REVIEW_QUEUE_SIZE` | `256` | **v0.77 登记**:有界审查队列长度。⚠️ **队列满 ⇒ 该条不做审查**(记忆本身早已落库,**绝不因队列满而丢记忆**),只记一条带来源的 WARN |
+| `MEMORY_REVIEW_SCAN_LIMIT` | `200` | **v0.77 登记**:每条审查在同一分区内最多看多少条既有记忆(top-K 上限) |
+| `MEMORY_REVIEW_TIMEOUT_SEC` | `20` | **v0.77 登记**:单条审查的墙钟上限(秒)。超时 ⇒ **不做任何改写**(记忆照旧保留)+ WARN |
+| (`MEMORY_REVIEW_*` 的共用口径) | — | 上面四个键 **`≤0` 一律视为「用默认」**(由 memory 包 normalize 收敛,避免一个笔误让进程起不来);召回门**复用 `MEMORY_TIDY_RECALL_THRESHOLD`**,不另立键。⚠️ **本版只登记已实现的部分**:审查的**读侧 / 回滚目前没有 HTTP 端点**(见 §7.2) |
+
+> **上面 13 个键(记忆整理 8 + 后台思考心跳 3 + 安静时段 2)自 v0.68 起可经 `GET /api/v1/admin/config-facts` 读到**「进程生效值 + 声明默认值」(3 个心跳键自 v0.75 起并入、`MEMORY_TIDY_COMPARE_LIMIT` 自 v0.77 起并入);在此之前面板没有任何 env 读取面。⚠️ `MEMORY_REVIEW_*` 四个键**不在**事实面里(那是「入库前审查」的另一组配置)。
+| `SUBAGENT_TIMEOUT_SEC` | `600` | 单个子代理整次运行时限 |
+| `MCP_SERVERS_JSON` | 空 | MCP server 注册表(JSON 数组)。⚠️ 扁平 `MCP_NAME`/`MCP_COMMAND`/… 为**未接线残留键,配了不生效** |
+| `CHANNEL_*` / `CHANNELS_JSON` | 见模板 | 渠道接入(`.env` 为兜底,面板 `channels.json` 优先) |
+| `CHANNEL_EVENT_RETENTION` | `720h` | **v0.67 登记,已接线**(E4 `afe9afb`;`.env.example` 同步于 `f9c5b56`):`channel_events` 保留期 —— 比文档的 `168h` 长,因为事件表的核心用途是**事后定案**(一周太短);`≤0` = 关闭 GC |
+| `CHANNEL_EVENT_GC_INTERVAL` | `1h` | **v0.67 登记,已接线**(E4 `afe9afb`):渠道事件 GC 扫描间隔(与 `DOCUMENT_GC_INTERVAL` 同范式) |
+| `CHANNEL_EVENT_COUNTER_FLUSH` | `30s` | **v0.67 登记,已接线**(E4 `afe9afb`):计数内存聚合的 flush 间隔。计数是**尽力而为的近似值**(进程崩丢 ≤30s),查询面以 `approx:true` 如实标注(见 §2.4(a)) |
+| `FLOW_GRAPH_RETENTION` | `168h` | **v0.73 新增,已接线**(切片 F2):`flow_graphs` 轮次图保留期;`≤0` = 关闭 GC(显式选择,「配了却不生效」不存在)。过期轮次从 `turns[]` 消失(见 §4.11(b)-5 / §7.4-20) |
+| `FLOW_GRAPH_GC_INTERVAL` | `1h` | **v0.73 新增,已接线**(切片 F2):链路图保留期清扫间隔(与 `CHANNEL_EVENT_GC_INTERVAL` / `DOCUMENT_GC_INTERVAL` 同范式;启动期扫一次 + 定时 + 失败不退出) |
+| `WEB_SEARCH_*`(`WEB_SEARCH_PROVIDER` / `_FALLBACK` / `_MAX_RESULTS` / `_ENDPOINT` / `_TIMEOUT` / 各家 `_API_KEY` / `_BASE_URL`) | 见模板 | **v0.78 起语义收窄(切片 PLG-4,运维视角的行为变更)**:web-search 的**可变源**改为 `$DATA_DIR/config/plugins/web-search.json`(0600、文件名 = 插件 id)。组合根**先读可变源、文件缺失才回落 `.env` 播种**(即 `.env` 只作**引导面**):**文件一旦落盘,改 `.env` 不再影响运行中的部署**。🔴 **不新增任何 env 键**(`.env` 已冻结);客户端一律走插件动作端点(§4.14(c)),**不直连该文件** |
+
+> ⚠️ **v0.78 的「`.env` 只播种」口径**(切片 PLG-4):这与 §5 开头「**只要文件存在且有效,运行时优先用它**,`.env` 只作兜底/播种层」是**同一条原则**在搜索 provider 上的落地 —— 此前 web-search 是纯 `.env` 面,v0.78 起它也有了面板/文件配置面。**无 env 键新增/删除**;模板里既有 `WEB_SEARCH_*` 键**保持现状**。
+
+**与契约相关但「配了不生效」的键**:
+
+- **`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 未接入,预留占位、当前不生效。
+
+---
+
+## 6. 变更记录
+
+> 只保留最近若干版(**v0.60 起**);更早的全部历史见归档 `Docs/archive/api-client.v0.65.md` §5(v0.28 → v0.59)。
+> 每版一句话 + 三档标注。**三档定义见 [§0.4](#04-三档标注惯例)。**
+
+### v0.85(当前契约版本)
+
+**「整理不再只产建议」版 —— 新增 1 条端点(B25)+ 一处行为变更(轮末自动落地)+ `link` 候选停产**(切片 TIDY-AUTO `6d92536`;依据:主人 2026-10-07 口径「该删就删,怎么这个记忆整理机制这么保守呢」;`design/23 §7/§13/§14`。)
+
+- **① 新端点 `POST /api/v1/admin/memories/candidates/reject?action=&limit=`(B25)**:按**动作**批量把 `pending` 候选标成 `rejected`。`action` **必填 + 闭集**;`limit` 缺省 1000 / 硬顶 10000;返回 `{action,limit,total,rejected,truncated}`;审计 `memory.tidy.reject`。**幂等**(再来一次 `total:0`),**只改候选状态、绝不碰记忆行**。详见 **§2.2 B25 / §4.15(e)**。
+- **② 行为变更:轮末自动落地默认开**(可自动仅两类:字面重复 ∨ 事实等价 ∧ ≥0.95 的 `merge`;`one_off_event` 且不像习惯的 `archive`);六条护栏(软动作 / 每轮上限 50 / 审计 `auto=true` / 可 revert / 幂等 / 一键关回只产建议,开关落 `$DATA_DIR/config/memory-tidy.json`,**零新 env 键**)。**不自动**:`reclassify`、低置信 merge、跨分区/跨角色、改正文、受限来源。详见 **§4.15(e)**。
+- **③ `link` 候选停产**:`design/23 §14` 把它列为未细化(无落点无模型),`applyable()` 也不认它 ⇒ 产出它只是噪音。存量清理由 B25 走(主会话已执行)。`TidyLink` 常量保留**只为读历史行**。
+- **④ 计数**:`writeAuth` **24 → 25**、总数 **78 → 79**(`14 + 25 + 40`;`routes.go` 核:`writeAuth` 挂载的 admin 路由实有 27,按历史口径减 2 ⇒ 25)。
+- **⑤ 诚实边界**:**未部署**(要一次 `build` + `restart`);**自动档的真机行为未验**;`reclassify` 仍全部人工;`TidyRuleVersion` **刻意未升版**(不改变任何判定结论;升版会让既有候选按幂等键复制重复行);面板「整理候选」页签仍未落地(前端切片)。
+
+### v0.84(上一版)
+
+**「显示名 + 段落标签硬约束」版 —— 一处形状变更(插件动作响应)+ 一处新增产出侧硬约束 + 一条模型面登记**(依据:主人报障「下拉里全给英文名,你甚至不愿意带个括号把中文名写上去」;安卓端登记 `正文
` 畸形;`design/28 §1 M5 / §8 B5` 的浏览器降级档。切片:显示名 `c46053e`、段落修复 `8051906`、provider 配置面去 env 化 `bc1dee3`、搜索 linkless 修复 `2c56bc9`。)
+
+- **① 形状变更:`plugin.providers` 候选从裸字符串数组 → 对象数组 `{value,label}`**。见 **§4.14(c)** 与 §2.1 A14。根因**不是**漏写文案,而是**整条链路没有「显示名」的位置**(字段声明只有 `optionsFrom`,动作回的就是 `KnownNames()` 的纯 id 数组)。中文名逐字:`SearXNG 本地兜底(searxng)` / `智谱 BigModel(zhipu)` / `阿里云 IQS 信息查询(aliyun_iqs)` / `博查 Bocha(bocha)` / `火山引擎·豆包搜索(volcengine)` / `百度千帆 AI 搜索(baidu_qianfan)`。🔴 **`label` 仅供显示、`value` 才是落盘值**;前端解码器兼容三种形状 ⇒ 其他插件零影响;表格按 `value→label` 索引显示、查不到回落原值。
+- **② 新增产出侧硬约束:助手文本不得出现未配对的段落标签**(落单 `` / `
`)。见 **§4.16**。修的是安卓登记的 `正文
`;**两半根因都在产出侧**(规整器原样放过 + 在「末段漏 `
`」时亲手制造),另修一个幂等漏洞。**历史行不回填**、消费方**仍应保留**容错。
+- **③ 模型面登记(非客户端契约面)**:`web-fetch` 的浏览器渲染降级档已接线,返回体用 `fetchedBy`(`direct` / `browser`)**如实标注**正文实际来自哪条路;两条档的失败各自留痕,**绝不静默替换**,被 SSRF 拒绝的目标**不换路**。
+- **④ provider 配置面「去 env 化」(对客户端零形状影响,登记供跨端对照)**:新增 provider 不再需要新的 `.env` 键 —— 面板文件 `$DATA_DIR/config/plugins/web-search.json` **是权威**,`.env` 只在文件缺失/为空时**播种**(文件一旦存在,「文件里没有的家」**不从 env 复活** —— 这是既有回归用例钉住的口径,本次未改);各家「没配 baseUrl 就回落代码默认值」的默认值表移进接入位表(`search.Descriptor.DefaultBaseURL`),**逐字未变**。
+- **行为变更**:**有一处(且是"变真")**:① 让「文件里没有的家不从 env 复活」这条既有口径在**新家**上也成立(此前新家根本进不来);② 段落标签约束让产出**不再畸形**(对不产畸形的部署零影响)。
+- **破坏性**:**无**(`plugin.providers` 只改**候选**形状且前端三形状兼容;无键被删、无端点被删、无既有语义被改)。
+- **计数**:**无新端点** ⇒ §2.0 权限矩阵维持 **14 + 24 + 40 = 78**(§2.2 B1–B24)。
+- **验证证据(本版实测)**:`go build ./...` + 各包 `vet` 绿;相关包测试全绿;三处变异由主会话**亲自**复现打红(把 `option.value` 换成 `option.label` ⇒ 渲染测试红,输出正是会写坏配置的 `"bocha, 智谱 BigModel(zhipu)"`;摘掉配对修复接线 ⇒ 段落用例红;把「文件里的 key」忽略 ⇒ 4 条 provider 投影用例红)。
+- ⚠️ **诚实边界**:① **`web-search` 真机工具路径在登记时仍有一处未部署的修复**(详见 v0.84 横幅与 §7.2)—— 已推库、**未 build/restart**;② 显示名的**真机浏览器点击**未跑(项目无 jsdom,证据止于渲染层)—— **且 `internal/webui/dist` 是 `go:embed` 进二进制的,前端改动必须重新 `build` 才生效**(子代理曾误称"纯前端改动不需要重启",已纠正);③ **`viewer` 角色看不到 form 区块** ⇒ 表格对 viewer 仍显示裸 id(未做);④ 腾讯云元宝 / 秘塔两家的适配**尚未实现**(文档已逐页落盘)。
+
+### v0.83(上一版)
+
+**记忆整理「应用面」—— 五条 `writeAuth` 新端点 + 读侧 status 闸(行为修复)**(切片 TIDY-1 `454ddb0` + `9126d65` + `f847769`;依据:`design/23` §7/§8/§10/§11;主人实测口径「候选 77 条全 pending、两天一条没应用」+「重复无效记忆都一千多条了」。)
+
+- **① 五条新端点(B20–B24)**:`GET …/memories/candidates`(候选列表,只读不审计)、`POST …/candidates/{id}/apply`(单条应用)、`POST …/candidates/apply?runId=&limit=`(批量;`runId` 省略 = 全部待定)、`POST …/candidates/{id}/revert`(单条回滚)、`POST …/memories/tidy/run`(手动真跑,写候选 + 留痕、**不自动应用**)。语义与逐字段形状见 **§2.2 B20–B24** 与 **§4.15**。审计:应用/回滚走 `memory.tidy.apply` / `memory.tidy.revert`(**只记计数与动作、不带正文**);真跑的 `memory.tidy.run` 由**领域侧**记(一次点击一条)。
+- **② 读侧 status 闸(行为修复,形状零变化)**:记忆的四条**列表**读路径(`ListByPersona` / `List` / `ListScope` / `ListProject`)现在只回 `status='active'` ⇒ 被软合并(`'merged'`)/被归档(`'archived'`)的记忆**不再被召回、不再进上下文、不再出现在 `GET /admin/memories`**。修的是「软动作写了 status,读侧却不看 ⇒ 清了也白清」(生产库已有 10 条 `merged` 行是现成反例)。🔴 **两处刻意不过闸**:`Get`(撤销软合并要先读得到被吸收的行)与 `GET /admin/memories/reviews`(留痕表本身没有 status 概念,且它是查看入口 —— 加闸等于审计自我抹除)。
+- **③ 调度不再饿死(对客户端零形状影响)**:整理由「ticker 从进程启动算起、重启十几次就永远走不完 24h」改为**到点即跑 + catch-up**,以 `memory_tidy_runs` 留痕行为**持久事实**判「上次跑过没有」;配 B24 作手动 panic button。
+- **行为变更**:**有(一处)**:② 让软删行从检索/上下文/面板里**真的消失** —— 这正是本版的目的;对**不软删任何东西**的部署,行为与 v0.82 逐字相同。
+- **破坏性**:**无**(五条都是新端点;② 是读侧收紧,不删键、不改键义、不改既有端点形状)。
+- **计数**:`writeAuth` 段 **19 → 24**、管理路由总数 **73 → 78**(`14 + 24 + 40`;`routes.go` 核:经 `writeAuth` 挂载的 admin 路由实有 26,按历史口径减 §2.4(a) 的 `channels/events` 与 `.../counters` ⇒ 24)。§2.2 行数 **B1–B24**。
+- **验证证据(本版实测)**:`go build ./...` + `go vet` 绿;`internal/runtime/memory` 包全量测试绿 + 新增读路径逐条断言(4 条列表 + 检索端到端「内容逐字相同的两条,软合并后只召回一条」+ `Get` 反向护栏);`internal/services/gateway` 新增面板端到端用例(真 `sql.DB` + 真 store + 真 HTTP handler);**两组变异**(overlay 把闸门改成恒真)都被打红。
+- ⚠️ **诚实边界**:① **生产库全程零动作**(1191 行记忆 / 77 条候选都还没应用或清理 —— 交付的是**能力**);② **面板真机端到端未跑**(前端操作面在本版登记时未落地,并行切片在做);③ 旧候选 `run_id` 为空串 ⇒ 只能用「全部待定」档清;④ 候选表 `run_id` 列只在存储包前滚、**未**入迁移 `0019`;⑤ 远端 embedder 对同一文本返回不同向量 ⇒ **语义召回不可复现**(不影响规则层与状态机,但影响「候选是否可复现」)。
+
+### v0.82(上一版)
+
+**三面增量 —— ① `tool_execution.end` 带可选 `resultSummary`(工具执行结果实时可见,切片 Y1 `215eedf`)|② 自排的成本可见面:`?trigger=` 服务端筛选 + 新端点 `GET /admin/backthink/self-wake`(切片 Y3 `4213b26`)|③ `browser` 插件的管理面板与六个动作(切片 Y4 `e0a6bca`)**(依据:安卓端按 ① 把工具卡 UI 定死;`design/12 §N C.5/C.6` 的自排成本口径与筛选;`design/28 §8 B4` 的面板入口。)
+
+- **① `tool_execution.end` 的 `data` 新增可选 `resultSummary`(string)**:`{tool, phase, id?, delta?, resultSummary?}`。数据来源 = **loop 侧**在工具返回值落定的那一刻生成(`internal/runtime/loop/tool_frame_summary.go`),走的是**与出图同一条**脱敏路径(`trace.RedactText`:凭据形状清洗 + 截断),**不从上游原文反解、不新开脱敏通道**。修的是「实时只看到工具名与状态、要事后翻回看面才知道结果」。**§3.4.2**
+- 🔴 **缺席语义(安卓端按此编程)**:**键在场 ⇒ 渲染结果区;键缺席 ⇒ 只显示工具名与状态**。**只有「真的没有结果」才缺席**:调用成功但结果为空(nil / 空串)、或错误文本本身为空。**绝不用空串顶位**(空串会让客户端渲染出一个空的结果区)。`tool_execution.start` / `.update` **不带**该键。
+- 🔴 **失败也带(本版口径)**:调用**失败 / 取消 / 超时**的 `.end` 帧带的是**该次错误文本的脱敏渲染** —— 与**回看面**(那里失败调用回的是图上的 `error` 摘要)**给同一份说法**,客户端不必为「实时」与「回看」写两套判据。⚠️ **本版之前**(切片 Y1 的首版实现)失败时是**整键缺席**、回看面却有 `error` 摘要 ⇒ 同一次调用「实时看不到原因、事后看得到」;本版把这处张力收掉了。
+- **预算**:与回看面 `toolCalls[].resultSummary` **共用同一个常量与同一个截断函数**(`maxToolCallSummaryRunes = 500`,超长按 rune 截断、末位 `…`)。网关侧**只截断、不做二次脱敏**(与回看面「只搬运、不加工」同一口径)。
+- **② 自排的成本可见面(切片 Y3)**:
+ - `GET /api/v1/admin/backthink` 新增**可选** `?trigger=` **服务端筛选**(design/12 §N C.6)。取值域 = **闭集**(`heartbeat` / `startup` / `self_wake` + 本服务订阅的事件主题,真源 `backthink.TriggerFilterValues()`);**值未知或为空 ⇒ `400 invalid_trigger`**(**绝不静默忽略** —— 那会让面板在显示全部的同时声称「正在筛选」);合法值**下推 SQL** ⇒ `limit` 数的是**匹配**条数。🔴 **「筛选白名单是闭集」与「`trigger` 的显示取值域不封闭」不矛盾**(前者约束客户端传进来的参数、后者约束我们发出去的显示值),见 §4.10(e-1)。
+ - 新增 1 条 `writeAuth` 只读端点 **`GET /api/v1/admin/backthink/self-wake`**(design/12 §N C.5 的读侧)。响应 `{data:{available,running,reason?,configFile?,gate?}}`,`gate` = `{pending,used,limit,windowSec,defaultLimit,defaultWindowSec,limitIsDefault,windowIsDefault}`。**三态必须可区分**(面板不得把「看不到」画成「0 次」);`available:false` 时 `gate` **整键缺席**且 `reason` 说明原因、**HTTP 仍是 `200`**。🔴 **`limitIsDefault` / `windowIsDefault` 只说「生效值恰好等于代码默认值」**,**不是**「配置文件里没写这个键」(网关不读 `backthink.json`,与 B12 的 `isDefault` 同一诚实口径)。🔴 **它只读、不审计、不写库**,也**不含任何可改配置的入口**(改上限/窗口仍要改文件 + 重启)。**§2.2 B19 / §4.10(e-2)**
+ - 🔴 **同版改正一处已成假话的旧口径**:v0.81 写的「`request_think` **尚未接线** ⇒ `self_wake` 与 4 条审计动词在生产里不会出现」**已过期** —— 入口工具**已接线**(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`,切片 X2 `2d211c2`;启动日志自证 `self wake tool registered tool=request_think`)⇒ **它们在生产里会真的出现**。§4.10(d) 与 §7.1 已同步改。
+- **③ `browser` 插件的管理面板与六个动作(切片 Y4;design/28 §8 B4)**:`GET /api/v1/admin/panels` 的目录新增 **`browser` / `settings`**(「浏览器与截图」,`route = plugin/browser/settings`,4 个区块 = 1 个 `form` 配置 + 3 张只读 `table`),该插件声明 **6 个动作**(3 读 `plugin.getBrowserState` / `plugin.listShots` / `plugin.getFingerprint`;3 写 `plugin.saveBrowserConfig` / `plugin.shootPage` / `plugin.checkFingerprint`),**全部已登记生产处理器**,**不开任何新路由**(走既有 A14 通用动作分发)⇒ **§2.1 的 14 与总数不因本项变化**。保存 = **落盘 + 热重载**(重建工具 + 原子替换注册表条目 ⇒ 下一轮生效);保存前**本来有**可用工具而新配置让它不可用 ⇒ **回滚文件** + **`409 config_rejected`**;保存前**本来没有**可用工具 ⇒ 文件留下 + `200` + `applied:false` + `reason`(**不报错**,否则主人没法先配好)。🔴 **读面绝不启动浏览器**(面板的只读区块连 viewer 都能看,让「看一眼」产生进程外副作用是越界);真正会起浏览器的只有 `plugin.shootPage` / `plugin.checkFingerprint` 两个写动作(`plugin.saveBrowserConfig` 的「热重载」只在**重建工具**时按配置探测可执行文件)。**§4.14(d)**
+- **③ 附:模型面(非客户端契约面,登记供跨端对照)**:`browser` 工具的 `screenshot` 返回值新增 **`turnAttachment`**(闭集 `queued` / `no-session` / `outbox-full` / `disabled`,配一句如实的 `hint`)—— 让**模型与面板**都能看出「这张图到底进没进上下文」,而不是只在日志里。⚠️ **`queued` 与「进上下文」之间还有一环**:`loop.Options.TransformContext` 上挂的装配钩子(`cmd/cyrene/wire_browser_context.go`)负责**取走即消费**地把它并进本轮视图。
+- **行为变更**:**无**(既有键逐字未改;新增的都是**可选键 / 新端点 / 新声明**)。
+- **破坏性**:**无**(`resultSummary` 是新可选键,老客户端不读即行为逐字不变;`?trigger=` 缺席即不过滤;`GET /admin/panels` 多一条目录项 —— 按 `pluginIds` 差集渲染「未声明管理页面」的客户端零影响)。
+- **计数**:`writeAuth` 段 **18 → 19**(新增 B19 `GET /admin/backthink/self-wake`)、管理路由总数 **72 → 73**(`14 + 19 + 40`);§2.2 行数 **B1–B19**。**本版无其它新路由**(③ 走既有 A14)。
+- ⚠️ **诚实边界**:① **①③ 的形状取自「工作树实现」** —— 登记时 ① 的「失败也带」(`internal/runtime/loop/tool_frame_summary.go`)与 ③ 的**两个组合根挂点**(`cmd/cyrene/wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)`、`cmd/cyrene/wire_runtime.go` 的 `TransformContext: a.browserShotTransform(visionResolver),`)**都还在工作树、尚未提交** ⇒ **提交前生产里没有这些行为**;提交后请按 `git show` 逐字复核(见 §7.4-30 / §7.4-31);② ① 的**真机**(`:10942` 发布产物)端到端**未跑**;③ ② 的**真机面板端到端未跑**(面板写路径**未做**,只有读侧);④ ③ 的**真机 e2e 未跑**(既有 `wire_browser_e2e_test.go` 默认 `Skip`,需要真 Chrome + 可执行文件路径)。
+
+### v0.81(上一版)
+
+**一批增量 + 两处真缺陷修复 —— ① 她自排轮(`self_wake`)的用户可见面(形状登记;生产暂不可达)|② 时间轴新增 `retentionBoundaryReached` 并与 `truncated` 分意(切片 X1)|③ `round` 跨重启不再重复(切片 X1 的行为修复)**(依据:`design/12 §N C` 的自排切片 C1-1 `94da6bc` / C1-2 `ed50053`;`design/19 §7` 时间轴的保留期诚实口径与 §6.1 的「`round` 每会话单调」;`AGENTS §9.5` 的对照要求。)
+
+- **① 她自排轮的用户可见面(切片 C1-1 / C1-2)**:`GET /api/v1/admin/backthink` 的行 **`trigger` 新增取值 `"self_wake"`**(= 她自己排起的一轮思考;`trigger` 的取值域**不封闭**:`heartbeat` / `startup` / 事件主题 / `self_wake`);**新增 4 条审计动词** `backthink.self_wake.{queued,coalesced,capped,denied}`(`who` 恒 `backthink`,经既有 B9 `GET /admin/observability/audit` 可查,`result` 是可直接显示的中文)。**无新端点、无新字段表**(两处均为取值域的**加性**扩张)。边界:自排**只保证「她被叫醒思考」**,要不要真发出去仍由既有 `decide` 级联 + `PushGate` 决定(安静时段/限频/冷却照生效);每窗口上限(默认 **4 次/小时**)**不在契约里冻结**(**不新增 env 键** —— `.env` 已冻结)。🔴 **可达性**:**她的入口工具 `request_think` 尚未接线**(下一片在办,`thinktool.go` 尚未提交)⇒ **该取值与这 4 条动词在生产里目前还不会出现**;本版**登记形状**,客户端不得当活跃数据依赖。**§4.10(d)**
+- **② 时间轴的保留期信号落地 + `truncated` 分意(切片 X1)**:`GET /api/v1/admin/flow/timeline` 的响应新增 **`retentionBoundaryReached`**(bool)—— **`true` = 该会话确有更早的轮次已被保留期清扫删掉,且本次已取到它现存最早的一端**。判据三条**缺一不可**:① `retentionEnabled:true`;② `hasMore:false`;③ 现存最早一轮的**轮次号 > 1**(轮次号每会话自 1 起、跨重启单调 ⇒ 号更小的轮次曾落盘、又不在库,而 `flow_graphs` 的唯一删除者是保留期清扫)。⇒ **`truncated` 从此只表示「本次不是全部」**(分页未到底 / 落盘源未接线),**不携带**清扫信息;面板说「更早的轮次已过保留期」的**唯一**判据是 `retentionBoundaryReached:true`。⚠️ **盲区(写进契约,不掩盖)**:整段历史都被清扫 ⇒ `false`(**少说,绝不多说**);有轮次号未知的老行 ⇒ `false`;最早那一轮**落盘失败**(号已用掉、行从未进库)⇒ 可能**错报**为 `true`(概率低,只在库写失败时出现)。**§2.2 B18 / §4.11.1(a)(b)-3**
+- **③ `round` 跨重启不再重复(切片 X1 的行为修复)**:轮次号原本只是发布器**进程内**计数 ⇒ 重启就从 1 重来,而 `flow_graphs` 保留 168h ⇒ 时间轴按轮次号分组时把**重启前那一轮**与**重启后那一轮**并成**一块**(真机实测 `session=web`:同一分组键下三次真实轮被并成「第 1 轮 · 3 路」)。修法 = 发布器**本进程第一次**给某会话发号前,从落盘侧读一次**已落盘的最大轮次号**并播种计数器(`flowgraph.MaxRound`),同一会话的并发首轮会等这一次播种。⇒ 契约里「`round` 每会话单调」这句**真的成立**。⚠️ 边界:播种是**尽力而为**(读库失败只 WARN 并退回进程内计数、**不重试** ⇒ 仍可能重复,客户端此时应以 `startedAt` 为准);库里为空 / 历史被整段清扫 ⇒ 从 1 起。**§4.11.1(e)**
+- **行为变更**:**有(但都是「变真」而非「变形」)**:① 两处取值域**加性**扩张(只显示 `trigger` 原文的客户端零影响);② ③ 是**行为修复**(重启后轮次号不再重复 ⇒ 分组不再并块);② 是**字段加性**扩张 + 对既有 `truncated` 的**读数收紧**(此前把它当清扫信号读的客户端会发现自己原来读到的是假话)。
+- **破坏性**:**无**(无键被删、无键被改义;`truncated` 的**定义**未变,变的是「不得拿它当清扫信号」这句口径)。
+- **计数**:**无新端点** ⇒ §2.0 权限矩阵维持 **14 + 18 + 40 = 72**(§2.2 B1–B18 共 **18** 条;②③ 都是往既有 B18 的响应/行为上加东西)。
+- ⚠️ **诚实边界**:① 自排的**端到端生产验证未做**(入口工具未接线,见 ①);② **②③ 的形状与判据取自切片 X1 的「工作树实现」**(本版登记时该切片**尚未提交**,`f1fbc24` 之后仍在工作树)⇒ **commit 后请按 `git show` 逐字复核键名与判据**;③ ② 的三条盲区**没有测试能覆盖到「整段被清扫」那一支**(属设计登记的诚实边界);④ 时间轴**真机面板端到端**仍未跑(同 v0.80 ①);⑤ 同波其余切片(技能渐进披露 L2 `6475eb8`、示例技能 V3 `4a04ad4`、浏览器 B1/B2 V5 `f1fbc24`)**对客户端零形状影响**,故未逐条登记。⚠️ 括注原文写「浏览器是**模型面工具**,不进客户端契约面」—— **对 B1/B2 成立,但已部分过期**:v0.82 的 **B4**(浏览器管理面板,切片 Y4)**进**客户端契约面,见 §4.14(d)。
+
+### v0.80(上一版)
+
+**三批增量 —— ① 链路图时间轴出口(切片 W1)|② 搜索 provider 热重载 + 主 provider / 降级链(切片 W3)|③ 入站语音 AMR 也可回放(切片 W4)**(依据:主人 2026-10-06 要「web 后台的全链路流程图 = **所有消息按时间顺序串起来**,而不是只有最新一个来回」;面板的意义就是「不改 `.env` 不重启」⇒ 配置面必须真热生效;微信 / QQ 的 AMR 入站语音不落副本 = 无法回放 / 多端同步。)
+
+- **① 链路图时间轴出口(切片 W1,`design/19 §7`)**:新增 1 条 `writeAuth`(owner\|admin)**只读、不审计**端点 **`GET /api/v1/admin/flow/timeline`**(`?sessionId=&limit=&before=`)—— 把多轮链路图**按时间正序**返回,每轮 `{turnId,round,startedAt,endedAt,lanes[]}`(`lanes[]` 每项 `{turnId,graph}`,`graph` 与 `graph_update` 同形)。数据源 = **已落盘的 `flow_graphs`**(迁移 `0022`/`0023`,保留期 `FLOW_GRAPH_RETENTION` 默认 168h)**+ 内存里的最近一轮**(在飞 / 刚落盘失败的那一轮);`limit` 缺省 **50** / 硬上限 **200**(超上限钳住、`≤0` 同缺省、非整数 `400 bad_request`);`before` 游标取「开始时刻**严格早于**它」的轮次、响应回带 `nextBefore`(**原样回带即可连续翻页**,分页不重不漏);响应另给 `retentionHours` / `retentionEnabled` / `truncated` 供面板**如实**标注「更早的轮次已过保留期」。**未接线(内存 + 落盘都没接)⇒ `500 flow_unavailable`**,**绝不回空 `rounds`**(不假装「这个会话没有轮次」)。权限矩阵:`writeAuth` 段实有 **18** / 总数实有 **72**(**本版同时回补了自 v0.76 起的 −1 记账滞后**,见 §2.0)。🔴 **老端点 `GET /admin/flow/graph`(B13)逐字未改**(`admin_flow.go` 零改动,`routes.go` 只加一行路由 + 注释)⇒ **老客户端零影响**。**§2.0 / §2.2 B18 / §4.11.1**。
+- **② 搜索 provider 热重载 + 主 provider / 降级链(切片 W3;裁决 2026-10-06 #1 / #2)**:`plugin.saveProvider` 成功落盘后,组合根**按落盘后的配置重建 web-search 工具并原子替换能力注册表里的条目**(新增窄方法 `registry.ReplaceTool` —— **不是** `Remove`+`Register`:中间无空窗,且**保留运行期策略**)⇒ **下一轮对话即用新配置,不需要重启**(design/09 §5.5)。**保存是一次事务**:快照文件 → 落盘 → 重建 → 换条目;③ 失败 ⇒ **文件按字节回滚**(temp+rename)+ **`409 config_unstartable`**(会 make-下次起不来的配置绝不留在盘上);回滚也失败 ⇒ 如实 `500 config_write_failed`。响应新增机器可读事实 **`hotReload:{applied,registered}`**。声明面**增补两个文件级字段** `primary`(`select`)/ `fallback`(`multiSelect`,候选来源 `plugin:providers`)—— **缺位 = 不改动**;`primary` 给空串 / `off` / `none` = **关掉这把工具**(与 `WEB_SEARCH_PROVIDER` 共用同一套关闭语义);点名「已登记但未配齐」的家 ⇒ **`400 primary_not_configured`**(**写盘之前**就拒);`fallback` 元素未登记 ⇒ `400 unknown_provider`、形状错 ⇒ `400 invalid_payload`。`plugin.reauthProvider` 的 `values` **追加** `primary` / `fallback`(否则面板「清空降级链」在客户端三态写回里清不掉)。**新增错误码**:`primary_not_configured`(400) / `config_unstartable`(409)。🔴 **本版改正三处已成假话的旧口径**(此前写「面板保存后**不**热替换运行中的工具、需重启」、「面板**无**主 provider / 降级链字段」):§4.14(b) / §4.14(c) / §7.2 / §7.4-26 / §7.4-27 已改。⚠️ **触发面只有本动作**:直接改写可变源文件(不经面板)**不**触发热重载,仍需重启。**§4.1 / §4.14(b)(c) / §7.2**。
+- **③ 入站语音 AMR 也可回放(切片 W4;裁决 #11)**:组合根补齐 `channelbridge.Options.VoiceTranscoder`(`cmd/cyrene/wire_channels.go`)—— 注入的是 `wireTranscode` 装配的**同一个**引擎实例(与工具面 `transcode` 工具同源,不是第二条降级链)。⇒ **不可播格式的入站语音在落盘之前先转成 `.wav` 再落副本**(`mime: audio/wav`;本期唯一必需的转码路 = QQ OneBot v11 的 `amr → wav`),客户端的 `voice` 从此带副本事实(`name` / `mime` / `size` / `durationMs` / `url`);**此前这些键缺席**(走 `category=transcode_not_wired` 的诚实 WARN、不落副本)。**形状零变化**(DTO `voice` 的键集合与语义一字未改);**可播格式**(ogg/wav/mp3/m4a/aac/flac/webm/oga/opus)仍**原样落、一次转码都不发生**。稳定 `category` 列表**不变**(10 类),但 `transcode_not_wired` 在生产装配下**不再出现**(它现在只剩「引擎未装配 / 构造失败」这一档)。🔴 **本版改正 6 处已成假话的「组合根未注入 `VoiceTranscoder` ⇒ AMR 不落副本」**:§4.5(缺口① + 现状行)/ §7.1(对照注)/ §7.2(语音面行 ②)/ §7.4-16(前向提示)/ §7.4-29(②)/ 旧版本横幅与 §6 旧版条目(v0.67 / v0.78 / v0.79)。**§4.5 / §7.2 / §7.4-29**。
+- **行为变更**:① 无(新端点;老端点逐字未改);② **有**(面板保存由「需重启生效」变为**下一轮即生效**;新增两个可配字段与两个错误码;`plugin.saveProvider` / `plugin.reauthProvider` 的响应形状**加键**);③ 无(DTO `voice` 的键集合与语义未变,只是**此前缺席的键现在会出现**)。
+- **破坏性**:**无**(新端点 / 新可选键 / 新错误码;新增的响应键都是**加性**的,老客户端不读即行为不变)。
+- ⚠️ **诚实边界**:① 时间轴的**前端渲染 / 真机面板联调**不在后端出口的证据内;`graph_json` 破损行的 `500` 路径**无测试**;游标精度到**毫秒**(同一毫秒起跑的两个不同轮次在极端情况下可能被一并跳过 —— 理论边界,已写进 §4.11.1(b)-4)。② 热重载的**真机面板端到端未跑**、**真机探针未打真实上游**;**启用态不跨重启持久化**。③ 语音**生产端到端未跑**(红线:不重启后端)⇒ 证据是 `channelbridge` 行为单测(真 AMR 夹具 → 真引擎 → RIFF/WAVE)与 `cmd/cyrene` 的**源码级接线用例**;WS 实时帧 / 握手回放**仍不带 `voice`**;删除消息 / 会话时音频副本引用计数**仍不释放**。逐条见 **§7.2 / §7.3 / §7.4**。
+
+### v0.79(上一版)
+
+**四批增量 —— ① 插件面板动作真可用(切片 L1/PLG-2 的执行面接线)|② 「入库前审查」(D3)管理面落地(切片 L45)|③ 语音消息的用户可见面落地(切片 L45)|④ 链路图权威回合标识覆盖到全部编排回合(切片 CODE-TC-2,**唯一一处形状变化**)**(依据:面板动作此前一律 501 ⇒ 面板「点了没反应」;主人要「入库前审查可查、可回滚」;安卓/Web 要「语音能回放、消息能标出语音来源」;`toolCalls[]` 覆盖率此前只到真并行路 ⇒ 普通单路聊天与队首续跑回复回看不了工具。)
+
+- **① 插件面板动作真可用(切片 L1/PLG-2)**:插件管理器新增**动作处理器登记面**(`Manager.RegisterActions` → `Panels.RegisterActions`,取代单一全局执行器;**声明仍是唯一闸门** —— 登记≠可执行);组合根在 `wirePanelHub` 里登记内置 `web-search` 的**四个动作 + 下拉候选来源**(`plugin.providers`)⇒ `POST /api/v1/admin/plugins/{id}/actions/{action}` **不再一律 501** —— 真读写 `$DATA_DIR/config/plugins/web-search.json`(0600 + temp+rename,**重启仍生效**;**v0.80 起保存后即热重载**,见 §6 v0.80 ②)、探针**真发一次最小 HTTP 检索**。失败回**结构化非 2xx**(码/文案/状态由动作处理器给出,宿主只透传、状态越界回落 502):新增 `invalid_payload`(400) / `unknown_provider`(400) / `not_configured`(400) / `config_unreadable`(500) / `config_write_failed`(500),以及探针码 `unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown`(502);**`501 action_not_implemented` 只在「已声明但宿主未登记处理器」时出现**(**不再是兜底档**);兜底执行失败仍是 `502 action_failed`。审计仍 `what="plugin.action"`,**服务端日志新增附加字段 `code`**(**不进 DB 记录体、不含请求体**)。**§2.1 A14 / §4.1 / §4.14**。
+- **② 「入库前审查」(D3)的管理面(切片 L45)**:新增 2 条 `writeAuth`(owner\|admin)端点 —— `GET /api/v1/admin/memories/reviews?limit=`(留痕列表:`limit` 缺省 100、`≤0` 同缺省、>500 钳位、非整数 `400`;元素含 `decision ∈ skip\|merge\|modify\|create`、`mergedIds` 恒为非 null 数组、`createdAt` UTC RFC3339;🔴 **不含记忆正文**)与 `POST /api/v1/admin/memories/{id}/revert`(撤销一次**软合并**:被吸收行复原 `active`、从存活行 `merged_from` 摘除、`merge_count` 减 1;**行永不物理删除**)。未接线 ⇒ `500 memory_review_unavailable`(**绝不回空数组**);回滚落审计新动词 **`memory.review.revert`**(只记 item id)。权限矩阵:`writeAuth` 14 → **16**、总数 68 → **70**。**§2.0 / §2.2 B16/B17 / §4.1**。
+- **③ 语音消息的用户可见面(切片 L45)**:消息 DTO 的 **`voice`**(含服务端唯一派生的 `url`)在 **4 个 REST 读取面 + `message_edited`** 真实填充;新增 **`GET /api/v1/media/{name}`**(`writeAuth`;`audio/*` 内联 + `nosniff` + `Accept-Ranges` + `Cache-Control: private, max-age=86400, immutable`;**无 `ETag`**;`Range` ⇒ 206、条件请求 ⇒ 304、支持 `HEAD`、不可满足 `Range` ⇒ 416(**非 JSON 信封**);名字非法 / 穿越 / 缺失统一 `404 media_not_found`);副本落盘走新增 `documents.Store.SaveAudio`(内容寻址、0600、元数据进同一张 `documents` 表、并入同一套引用计数),`chat_messages.voice` 列读写两条路补齐(`history.Store.UpdateMessageVoice` / `VoiceColumns`)。⚠️ **残留缺口(如实)**:`message_persisted` / `channel_persisted` 帧与握手 `history` 回放**仍不带 `voice`**(需给 `history.Message` 加字段,未做;且 `message_persisted` 在落库那一刻发出,天生拿不到落库后才回填的值);组合根**未注入 `VoiceTranscoder`** ⇒ **AMR 不落副本**(可播格式照落)—— ✅ **该缺口已由 v0.80(切片 W4)修复**(组合根已注入,见 §6 v0.80 ③ / §4.5 / §7.2)。**§0.3 / §1.2 U19 / §3.4.4 / §4.1 / §4.4 / §4.5**。
+- **④ 🔴 链路图权威回合标识覆盖到全部编排回合(切片 CODE-TC-2)**:新增 `loop.FlowPublisher.StartRoundWithTurnID(ctx, owner, text)`(权威回合标识取自 ctx 的 `TurnIDFrom`),`run.go` 的**唯一生产建图点**改走它 ⇒ 编排器驱动的**每一路**(**普通单路回合**、队首续跑路、真并行路)的图都带与 `chat_messages.turn_id` **同值的 `turnId`** ⇒ 消息面 **`toolCalls[]` 覆盖率从「只有真并行路」扩大到「全部编排回合」**(普通单路聊天与队首续跑回复现在也能回看工具)。🔴 **唯一形状变化**:`graph.id` 的 `#{权威回合标识}` 后缀现在**单路也追加**(`web-7` → `web-7#turn-…`)—— 该键仍**不透明**、仍可能含 `#`;客户端**一律使用响应里的 `graph.id` / `turns[].turnId`**,**不要自己拼** `{会话}-{序号}`(自拼的老客户端**查不到新行**)。**缺席语义一字未改**:拿不到权威标识的写入路径(未接编排器的调用方、**后台思考** `sess-backthink`、旧后端)`turnId` 仍缺席、`toolCalls` 仍**整体缺席**(**绝不回空数组**);**v0.79 之前的单路历史行不回填**(`lane_turn_id` 为空且无可回填来源)。**§4.4 / §4.11 / §7.2 / §7.4-23**。
+- **行为变更**:① **有**(面板动作由「全 501」变为**真执行**)+ 新增一批错误码;② 无(新端点;老客户端零影响);③ 无(`voice` 是既有 DTO 的可选键、`GET /media/{name}` 是新端点);④ **有(唯一一处形状变化)**:`graph.id` 对单路也追加后缀 ⇒ 自拼 id 的读取方须改用响应值(**其余形状与缺席语义一字未改**)。
+- **破坏性**:**无**(新端点 / 新可选键 / 新错误码;④的形状变化只影响「自己拼 `graph.id`」的读取方,而本文档自 v0.73 起就写明该键**不透明、应从响应取**)。
+- ⚠️ **诚实边界**:① 面板动作**热重载未做**(保存后运行中的 web-search 工具不换接线,需重启)—— ✅ **已由 v0.80(切片 W3)修复**(见 §6 v0.80 ②)、**探针只用 `httptest` 假上游**(未对真实上游连通,**仍未做**);② 回滚端点与 `design/23 §11` 的 owner-only 口径**有差异**(待裁决)、留痕列表**无分页游标**、`memory_ingest_reviews` 无 TTL/GC;③ 语音**生产端到端未跑**、WS 实时帧/握手回放不带 `voice`、AMR 不落副本(✅ **已由 v0.80 修复**)、删除消息/会话时音频副本引用计数不释放;④ 单路 `toolCalls[]` 的**HTTP 出口端到端未由本切片确证**(写侧已验,网关读侧由主会话补验)、`turns[]` 仍按**路**列条目、真机未验。逐条见 **§7.2 / §7.4**。
+
+### v0.78(上一版)
+
+**六面增量 —— ① 消息读取面 `toolCalls[]` 落地(覆盖率受限)|② 插件管理面板接入点(`GET /admin/panels` + 插件动作分发端点 + `panels_changed` 帧)|③ 搜索 provider 可变源(`web-search` 插件)|④ 账号上下线落事件 + 渠道事件实时帧 `channel_event`|⑤ 微信 iLink 凭据三态(`authState`/`authDetail`)|⑥ 语音消息落库面(迁移 `0020`)**(依据:安卓侧要「回看这条回复当时调了什么工具」;design/09 §5.2.1 的插件面板接入点 + §5.4 的安全与权限;design/27 的网络搜索提供商 + design/13 §2.1 的插件配置落点;design/26 §2.6/§4.5 的账号状态事件与实时面;`errcode=-14` 被读成「过期」的误判风险;design/24 §10 的语音落库面。)
+
+- **① 消息读取面 `toolCalls[]` 落地(切片 CODE-TC)**:`GET /api/v1/sessions/{id}/messages` 的消息对象新增**可选** `toolCalls`(`omitempty`),每条 `{name,status,durationMs?,argsSummary?,resultSummary?}`(`name`/`status` 恒在,`status ∈ {ok,error,unknown}` 闭集三态;后三键图上给不出时**缺席**)。数据源 = **已落盘的轮次链路图** `flow_graphs`;🔴 **缺席语义**:图取不到 ⇒ 字段**整体缺席**,**绝不回 `[]`**(客户端只做正向断言)。**为打通读侧补齐**:迁移 `0023_flow_graphs_lane_turn_id` 给 `flow_graphs` 加**按路查询键** `lane_turn_id`(等值索引 `idx_flow_graphs_lane_turn_id (session_id, lane_turn_id)`)+ 回填;`FlowGraphStore` 增只读方法 `GraphsByTurnID`(一页一次 `IN` 批量查,禁 N+1);`history.go` 的 `handleListMessages` 在编码前调 `attachToolCalls`。**只在该出口填充**(其余 DTO 出口逐字不变,有单测钉住)。⚠️ **覆盖率受限(如实,v0.78 时)**:只有**真并行路**的图带权威 `turnId` ⇒ **普通单路回合的回复查不到图、`toolCalls` 缺席**(补齐在 `internal/runtime/loop/**` 写侧,**不在本版**)。→ **✅ v0.79 已补齐**(切片 CODE-TC-2:编排器驱动的每一路都带权威 `turnId` ⇒ 覆盖率扩大到**全部编排回合**;见 §6 v0.79 ④)。**§4.4 / §7.2 / §7.4-23 / §2.2 B13**。
+- **② 插件管理面板接入点(切片 PLG-2,纯增量为主)**:新增 1 条 viewer 读端点 **`GET /api/v1/admin/panels`**(**按调用者角色过滤**后的接入点快照 `{role,groups[],pluginIds[]}`,viewer 只拿只读区块;未接线 ⇒ `501 panels_unavailable`)与 1 条动作分发端点 **`POST /api/v1/admin/plugins/{id}/actions/{action}`**(**只接受该插件声明过的动作名**,未声明 ⇒ `404 action_not_declared`;写动作 owner/admin 收紧 ⇒ 未授权 `403 forbidden`;停用插件 ⇒ `409 plugin_disabled`;**每次结算都落审计** `what="plugin.action"`;**插件只给动作名、不给 URL** ⇒ 不开 SSRF 面);WS 下行帧 **`panels_changed`**(`{reason,pluginId}`,`reason ∈ {declare,install,uninstall,enable,disable}`,**只发 owner/admin**,**不带内容**⇒ 客户端按角色重拉快照)。权限矩阵:`viewerAuth` 12 → **14**、总数 66 → **68**。🔴 **v0.78 时生产路径硬事实**:**无任何生产动作执行器** ⇒ **当时所有已声明动作都回 `501 action_not_implemented`** —— **已由 v0.79 接线**(见 §6 v0.79 ① 与 §4.14)。**§2.1 A13/A14 / §3.4.5 / §4.14(a)(b)**。
+- **③ 搜索 provider 可变源(切片 PLG-4)**:插件 `web-search` 新增可变源 `$DATA_DIR/config/plugins/web-search.json`(**0600**、temp+rename、文件名 = 插件 id),组合根「**先可变源、缺失回落 `.env` 播种**」;面板数据面 = 四个动作 `plugin.listProviders` / `plugin.saveProvider` / `plugin.testProvider` / `plugin.reauthProvider`(表格行 `{provider,state,enabled}`,`state ∈ {unknown,disabled,not_configured,ok}`;`testProvider` 的 `errorCode` 闭集十项,⚠️ **`no_results` 时「空结果 ≠ 不通」**)。🔴 **不新增任何 env 键**(`.env` 已冻结);**`.env` 的 `WEB_SEARCH_*` 自此只在文件缺失时生效**(运维视角的行为变更,如实登记)。**§4.14(c) / §5**。
+- **④ 账号上下线落事件 + 渠道事件实时帧(切片 E7)**:`internal/runtime/events` 增「账号状态迁移」这一路 —— 三态归一(**未知也照常落行**,不整类丢弃)→ 落一行 `tier=t2` / `category=lifecycle`(**复用迁移 `0021`**)→ 经**既有进程内 bus** 的主题 `channel.event` 推成 WS 下行帧 **`channel_event`**(`data.event` 与 REST `events[]` **同一个对象**;⚠️ **实时帧的 `id` 键可能整缺**;**只发 owner/admin**,**viewer 走 REST 只读档**)。采样常驻(约 60s 一拍,**不看面板也在跑**)。⚠️ **当前只覆盖账号状态迁移这一路**(其余渠道事件的实时装饰器**未接线**,见 §7.2);采样通道目前唯一来源是 `qq-obv11` 的探活。**§3.4.5**。
+- **⑤ 微信 iLink 凭据三态(切片 CODE-F3)**:`GET /api/v1/admin/channels/{platform}/login-status` 新增**可选** `authState` / `authDetail`(`"active"` 有效 / `"rejected"` **被拒且需重扫** / `"unknown"` 未知;**闭集**)。🔴 **文案口径写死**:**`errcode=-14` 是「被拒」不是「过期」**、**未知原因不许写成「过期」/「未连接」**(响应体全文由测试钉住)。**键缺席 = 该账号没有这条状态面**,≠ `unknown`。**无新端点、无新路由**(沿用 v0.39 起的端点)。⚠️ `GET /admin/channels` 的 `accounts[]` **每账号**三态**未实现**(见 §7.2)。**§2.4(c)**。
+- **⑥ 语音消息落库面(切片 CODE-VOICE)**:迁移 `0020_chat_message_voice` 给 `chat_messages` 加 `voice TEXT NOT NULL DEFAULT ''`(JSON 单对象,形状 = DTO `voice` 减去 `url`;**刻意不建索引**);`internal/runtime/voice/**` 新包(`Source`/`Status` 闭集、`Ref` 编解码、格式级可播表 + `amr→wav` 转码归一);`channelbridge` 入站语音「归一 → **闸门之后**落副本 → 落库后写 `voice` 列」全链打通(落盘内容寻址、复用文档存储与 GC、**不进文档抽取/注入链路**)。⚠️ **用户可见面(DTO `voice` + `GET /api/v1/media/{name}`)在 v0.78 时未实现,已由 v0.79 落地**(切片 L45,见 §6 v0.79 ③);**残留缺口**(WS 实时帧/握手回放不带 `voice`;AMR 那一项**已由 v0.80 修复**)见 §4.5 / §7.2。唯一已生效的**客户端可见**变化:IM 语音在「格式不在可转写白名单内」时正文诚实提示改口径(**形状不变**,客户端本就不该按正文文案分支)。**§4.5 / §7.2**。
+- **行为变更**:① 无(新可选键 + 新迁移列/索引);② 无(新端点 / 新帧 / 新可选键;老客户端零影响)—— ⚠️ 但**新增了 `502` 状态码**(仅插件动作执行器失败路径)与一批插件动作错误码;③ **有**(运维视角:`.env` 的 `WEB_SEARCH_*` 仅在可变源文件缺失时生效);④ 无(新帧 + 新事件行;REST 形状未变);⑤ 无(两个可选键;`status`/`botId`/`botToken`/`err` 与 v0.39 起逐字一致);⑥ **有(轻微)**:IM 语音「不可转写格式」的正文诚实提示**改口径**(**形状不变**)。
+- **破坏性**:**无**。
+- ⚠️ **诚实边界**:① `toolCalls[]` **v0.78 时覆盖率只到真并行路**(普通单路回合查不到图 ⇒ 缺席)—— **已由 v0.79 补齐**(见 §6 v0.79 ④),且**未做真机端到端**(真并行路的 `lane_turn_id` 与 `chat_messages.turn_id` 逐字相等仍是**代码级**推论);② 插件动作**生产无执行器**(全回 501)—— **已由 v0.79 接线**(见 §6 v0.79 ①)、热装插件的声明来源缺、`panels_changed` 对 viewer 不可见、启用态**不跨重启持久化**、审计持久化记录**未含 pluginId/action**(都在 §7.2);③ `web-search` 声明已就绪但**「面板加一家 provider」未做真机端到端**;④ `channel_event` **只覆盖账号状态迁移这一路**(组合根装饰器未接线),且**未做真机端到端**;⑤ iLink `rejected` 分支**只有单测/httptest 证据**(真实 `errcode=-14` 至今 0 次观测);⑥ 语音**生产端到端未跑** —— **用户可见面已由 v0.79 落地**(见 §6 v0.79 ③),但 WS 帧/握手回放仍不带 `voice`;AMR 不落副本**已由 v0.80 修复**(见 §6 v0.80 ③)。
+
+### v0.77(上一版)
+
+**链路图按路分桶(FV2)+ 记忆整理护栏抬高 / 诚实截断 / 试算异步落地(MT-A)+ 「入库前审查」env 键登记(MT-B)—— 三批都是纯增量**(依据:主人 2026-10-05 报「并行两路时流程图互相顶掉」、以及「工具回看面要先让链路图带上按路的回合标识」;「护栏被打满 ⇒ 候选数由上限而不是规则决定」要治;`design/23 §15.1 D3` 的入库前审查上线)。
+
+- **① `graph_update` 按路分桶(切片 FV2)**:帧 `data` 新增**可选** `turnId`(这张图所属**路**的权威回合标识,与 `turn_begin` / `steer_ack` / 流帧帧壳同值)与 `round`(会话轮次号);图按 `{会话}#{路}` **归属桶**建(单路的桶逐字等于会话 id ⇒ **单路行为逐字不变**),因此真并行时**每一路各发自己的帧、各落一行**(`flow_graphs` 的唯一键 `(session_id, turn_id)` 不再把另一路整行顶掉)。`GET /api/v1/admin/flow/graph` 响应新增 **`lanes[]`**(最近一轮每一路的图 `[{turnId,graph}]`,按路开始时间升序,恒为非 null 数组;`graph` 仍是「最新一路」⇒ **老的读取方零改动**),前端在 ≥2 路时按**并列泳道**呈现 —— **§2.2 B13 / §3.4.5 / §4.11(a)(d) / §7.2 / §7.4-19**。⚠️ 留白:`turns[]` 仍按**路**列条目(一轮两路显示两条),按轮分组需新迁移 `0023`(**未做**);Agents 页 / 后台思考页仍只显示最新一路。
+- **② 记忆整理护栏抬高 + 诚实截断 + 试算异步落地(切片 MT-A)**:每分区候选上限 `MEMORY_TIDY_CANDIDATE_LIMIT` 默认 **`20 → 200`**;两两比对上限从**写死的 `400`** 改为 env 键 **`MEMORY_TIDY_COMPARE_LIMIT`(默认 `2000`)**;配置事实面 **12 → 13 条**;`truncated` 新增第 4 种标签 **`(budget)`**(被**墙钟时间预算**截断:整分区跳过,或两两比对中途收手 —— 成因是**本次调用的时间上限**而不是配置配额);**v0.76 ③ 的加性异步已落地**(`?async=1` + B15 轮询:并发**复用既有 `runId`**、与同步**同一个 3 分钟**墙钟、`done` 的 `result` 与同步 `data` **逐字节同形**、`running` 时 `result`/`error` **双缺席**、重启后旧 `runId` 如实 `404`)—— **§4.10(a) / §4.12 / §4.12.1 / §5**。
+- **③ 「入库前审查」(D3)的 4 个旋钮(切片 MT-B)**:`MEMORY_REVIEW_ENABLED`(`true`)/ `MEMORY_REVIEW_QUEUE_SIZE`(`256`)/ `MEMORY_REVIEW_SCAN_LIMIT`(`200`)/ `MEMORY_REVIEW_TIMEOUT_SEC`(`20`)—— 队列满 ⇒ **该条不审查但记忆不丢**(只 WARN)、超时 ⇒ **零改写** + WARN、`≤0` = 用默认、召回门**复用** `MEMORY_TIDY_RECALL_THRESHOLD`。🔴 **这 4 个键不进 `.env` 模板**(主人 2026-10-06:`.env` 冻结为「只保证最小化功能可跑」的引导面、**不再新增键**)⇒ **有效行为 = 上面这组代码默认值**,调优入口将由**配置文件 / 面板**提供(**未接线**)。⚠️ 该切片的**面板读侧端点(`GET /admin/memories/reviews`)与回滚端点(`POST /admin/memories/{id}/revert`)目前没有 HTTP 入口**,**本版不登记为可用**(见 §7.2)。
+- **行为变更**:无(① 两个新键可选、`lanes[]` 纯增量、单路行为逐字不变;② 只改默认值 / 新增一个截断标签,同步试算的响应形状未变;③ 只多了一组**以代码默认值生效**的旋钮,**不进 `.env`**、不改变任何既有行为)。
+- **破坏性**:无。
+- **⚠️ 诚实边界**:① 真并行**未做真机端到端**(线上产物是旧构建;真并行是在单测层用「真 `Loop.Run` × 2 + 网关照同的 ctx 通道」复现的)—— 见 §7.4-24;② 上面 ②「抬高后真机 `compared == scanned`」**尚未被真机证实**(只有单测与量级推算支持);③ MT-B 的入库前审查**未在生产库上跑过**。
+
+### v0.76(上一版 · ⚠️ **逐条状态不同 —— ①已由 v0.78 实现(覆盖率受限);②③已实现**)
+
+**三面增量 —— ① 消息读取面 `toolCalls[]`(工具回看 · **✅ 已由 v0.78 落地**)|② 未读面 `unreadCount`(多端共享单游标 · **已实现**)|③ 记忆整理试算的加性异步(**已由 v0.77 落地校正**)**(依据:安卓侧 `Cyrene-For-Android/devdocs/02` 明确「工具回看面比未读面更重要、且要带工具输出/摘要」,以及主人 2026-10-05 裁定的「抬高 + 治入库太宽」导致试算更慢)。
+
+- **① 工具回看面(✅ 已在 v0.78 实现 —— 见 §6 v0.78 ① 与 §4.4;**覆盖率受限**)**:`GET /api/v1/sessions/{id}/messages` 的消息对象加可选 `toolCalls`(`omitempty`)—— 每条含 `name` / `status`(`ok`|`error`|`unknown`)/ `durationMs` / `argsSummary` / `resultSummary`(后两者是**图里已有的、已脱敏**内容并按上限截断)。数据源 = **已落盘的轮次链路图**(`flow_graphs`,迁移 `0022`;**v0.78 补迁移 `0023` 的按路查询键**,保留期 168h),**不从正文反解、不从内存态猜**。🔴 **缺席语义**:图取不到 ⇒ 字段**整体缺席**(**绝不回空数组**冒充「没调过工具」)。修的是安卓侧「回看这条回复时看不到它当时调了什么」。**⚠️ 历史说明(v0.76 登记时未实现的原因,实测确认非推断)**:`chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源**(前者 = 编排器生成的随机路标识 `turn-<16 hex>`,后者 = `trace.FlowGraph.ID`,进程内递增的建图序号)⇒ **无法按消息 join 到它那一轮的图**。⚠️ **v0.78 校正(切片 CODE-TC)**:v0.77 已给链路图加**按路的回合标识**,v0.78 又补上**按路查图的读侧入口**(迁移 `0023` 的 `lane_turn_id` + `GraphsByTurnID`)⇒ `toolCalls[]` **已实现**;但🔴 **v0.78 时只有真并行路能回看**(普通单路回合的图当时不带权威 `turnId`)—— **已由 v0.79(切片 CODE-TC-2)补齐**:编排器驱动的每一路都带权威标识 ⇒ 覆盖率扩大到**全部编排回合**(见 §6 v0.79 ④ / **§4.4**、§7.2、§7.4-23)。
+- **② 未读面(纯增量 · ✅ 已实现)**:`message_persisted` 的 `data` 加可选 `unreadCount`(`omitempty`,**只在 >0 且确实算得出来时**出现;**缺席 = 老行为**),`GET /api/v1/sessions` 的列表项同键(**该键自 D4 起就已在**,本版未改它)。口径 = **绝对未读 + 主人单一已读游标(`chat_sessions.read_at`,迁移 `0010`)⇒ 多端共享**:Web 读了,移动端的红点也清 —— 这是与安卓侧**已对齐**的取舍(他们明确赞成),**不是缺陷**,不许改成「每端独立未读」而不经裁定。形状、缺席语义与三种缺席情形见 **§4.13**;帧表已同步(**§3.4.4**)。
+- **③ 记忆整理试算加性异步**:`POST …/memories/tidy/dry-run?async=1` ⇒ `202` + `runId`(新增 B15 轮询端点)。**不带 `async` 仍是同步** ⇒ 老客户端(含现有面板)逐字不变。结果活在**进程内存**(TTL 30 min、并发 1、重启即失效 ⇒ 如实 `404`)。⚠️ **v0.77 落地校正**:本项登记的时点标注「契约先行」,**切片 MT-A 已实现**,实现口径见 **§4.12.1**(并发=**复用**既有 `runId`、与同步**同一个 3 分钟**墙钟、`result` 与同步 `data` **逐字节同形**、新错误码 **`tidy_run_not_found`**)。
+- **行为变更**:无(②只在既有帧里加可选键;③只在显式带参时改变路径;①未落地 ⇒ 无变更)。
+- **破坏性**:无。
+- **⚠️ 迁移号预告**:本版实现若需新迁移,编号从 **`0023`** 起(`0020` 预留给语音切片、`0021`/`0022` 已用)。
+
+### v0.75
+
+**记忆整理「试算」端点 —— 新增 `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:[]` 来表示失败 —— 空数组唯一含义是「算过了,这一轮没有建议」。
+- **试算专属的严格**:语义召回不可用时,**试算报错而不静默降级**到词项余弦(否则面板会把「语义路径没跑起来」读成「没有候选」)。低峰定时轮与手动轮的降级口径**不变**。
+- **不审计、不调度**:它是只读计算(不写审计链),**不受安静时段约束**(窗口只是心跳调度口径),也**不改变**心跳节奏与周期。
+- **行为变更**:无。
+- **破坏性**:无(老客户端不发这个请求就零影响)。
+- ⚠️ **本版不做**:apply / reject / revert 候选的**写**端点仍是 `design/23 §11` 的未实现项(见 §7),面板当前只做「看建议」。
+
+### v0.74(上一版)
+
+**IM 路由的主动消息也补浏览器通知 —— `message_persisted` 新增可选结构化判据 `proactive:true`**(依据主人 2026-10-05 裁定 C 的必然推论「她说话该不该提醒我,与走哪条路由无关」;对齐 `design/12 §3` 的主动推送与 `design/16 §4.4` 的镜像口径)。
+
+- **纯增量**:
+ ① **`message_persisted` 的 `data` 新增可选键 `proactive:true`**:IM 路由(`im_bridge`)的主动推送在落库之后**追加**一条带该键的同类帧 —— 帧类型、角色门、`message` DTO 形状**一字未改**,只多一个布尔键(**§3.4.4**)。
+ ② 客户端据此走与 `proactive_message` **同一套**提醒逻辑(非前台补一发通知;去重键同为 `message.id`)。老客户端不读该键即行为与 v0.73 完全相同。
+- **修的是什么(根因)**:`auto` 提示在主人有已连接 IM 渠道时**优先走 IM**,而 IM 路由此前只发普通 `message_persisted` —— 浏览器通知挂在 `proactive_message` 上 ⇒ **实际最可能发生的路径恰恰不弹**。这就是 P0(v0.72)如实报回的边界。
+- **判据是结构化的**:主会话落库行的 `platform`/`adapter_id` **区分不出**主动推送与普通回复(主动推送写的是解析后的真实平台,例如 `weixin-ilink`,与普通回复行同一形状)⇒ 判据放在**落库那一刻的通知通道**上(`gateway.BroadcastProactivePersisted`),随帧顶到达客户端。**绝不**读正文关键词、**绝不**用提示词/模型判断(项目红线:身份与判定靠元数据)。
+- **不误伤**:普通消息(Web 回合 / IM 回复 / IM 镜像)走的仍是原入口 `BroadcastPersisted`,那条路径**永不**放该键 ⇒ 客户端不可能把普通消息误判成主动消息。客户端只应做 `data.proactive === true` 的严格相等判断。
+- **行为变更**:无(普通镜像帧照旧发一条;主会话里仍**恰好一条**行,P0 的「不双份」红线未动)。
+- **破坏性**:无。
+- ⚠️ **诚实边界**:① 判据**不落库**(不改 schema)⇒ 刷新后从历史读回的行不带它(预期:通知只对实时到达有意义)。② **落库失败时不发判据帧**:没有权威 `id` 的 `message_persisted` 会被客户端整帧丢弃,发它等于假装提醒过 —— 那种情况下只记 WARN。③ 「她主动说的话」在三类帧上到达的完整清单见 §3.4.4 与 §3.4.5。
+
+### v0.73(上一版)
+
+**链路图本地落盘 + 多轮历史回放 —— 图从「只活在进程内存」变成每轮一行落进本地库,`GET /api/v1/admin/flow/graph` 增量支持按 `turnId` 取历史轮**(依据主人 2026-10-05 追加要求「链路流程应该有本地数据,不然每次重载之后页面就没数据了」;对齐 `design/19 §4` 的 **P3**「多轮历史回放」—— v0.71 只做了其中最小一步)。
+
+- **纯增量**:
+ ① **落盘**:每轮收尾(`EndRound`)把该轮的图写进新表 `flow_graphs`(迁移 `0022`),**每轮一行**(不是每 span 一行);只落**发布会话白名单内**的会话(复用 `flowSessionAllowed`,与实时发布同一判据)。新 env 键 `FLOW_GRAPH_RETENTION`(默认 `168h`)/ `FLOW_GRAPH_GC_INTERVAL`(默认 `1h`),GC 口径照 `events`/`documents`(启动期扫一次 + 定时 + 失败不退出;`≤0` = 关闭;**不许无限增长**)。
+ ② **`GET /api/v1/admin/flow/graph` 新增 `?turnId=`**:省略 = 最近一轮(**先查内存 `LastGraph`,miss 才回退落盘** ⇒ 最新一轮永远最新);给了 = 精确取该落盘轮。
+ ③ **响应新增三键 `turnId` / `source` / `turns[]`**:`source` ∈ `live`/`persisted`/`none`;`turns[]`(`{turnId,createdAt}`,最新在前)供面板选历史轮 —— `graph`/`sessionId` 两键的既有语义一字未改。 —— **§2.2 B13 / §4.11**。
+ ④ 前端:图桶 `flowGraphs` 纳入 store 持久化(刷新即刻有图)+ 历史轮次选择 UI + 诚实文案(白名单外会话永远没有图;未跑新轮次前的旧轮次靠落盘)。
+- **行为变更**:无(`graph_update` 帧形状、角色门、发布会话范围全部未改;`graph`/`sessionId` 语义未改)。
+- **破坏性**:无(新查询参数可选、新响应键可选;老客户端不读即行为与 v0.71 相同)。
+- ⚠️ **诚实边界**:落盘只覆盖**保留期内**的轮次(默认 7 天)—— `turns[]` 不是全历史;`graph_update` 两路互顶的既有裁决**本版未动**(§7.4-19)。当前仍**没有图**的轮次见 §7.4-20。
+
+### v0.72(上一版)
+
+**主动推送可见性 —— 主动消息成为主会话里的一条真实消息,并给帧补上权威行 id**(依据主人 2026-10-05 报「后台思考的主动推送一个都没推出来」;对齐 `design/12 §3` 的主动推送与 `design/16 §4.4` 的镜像口径)。
+
+- **行为变更**:
+ ① **`proactive_message` 那条消息现在会落进主会话历史**(`web`、`role: assistant`、`mode: "chat"`、`turn_id` 留空)。此前后台思考走**活跃客户端路由**时只广播 WS 帧、**一行都不写** ⇒ 刷新/重载后既不显示气泡、也补不出历史;那句 `delivered to 2 active client(s)` 只表示「帧被 2 个连接收下」,**不代表进了会话**。顺序改为**先落库、再发帧**。
+ ② **帧新增两个可选键 `id` / `createdAt`**(同现/同省):这条消息在主会话历史里的权威行 id 与落库时间戳。客户端据此把实时气泡对齐到历史行(按 id 去重、刷新后由历史行接续)。**落库失败时两键缺席** —— 客户端不应把「没有 id」当成一条可持久化的消息。
+ ③ **没有在线 owner/admin 连接时也落库**:推送器如实回 `pushed=false`(没有活跃客户端),但那句话仍作为真实记录留在主会话里。
+ ④ **后台思考记录里的 `PushResult.Detail` 措辞**:活跃客户端路由改为 `delivered to N active client(s), persisted to main session`(未落库时写明 `not persisted…`,无在线客户端但已落库时写明 `no active client, recorded in main session`)—— 面板因此不会把「帧发出去了、刷新就没了」误读成「已推送」。
+- **纯增量**:帧的形状只在**新增可选键**;落库行的形状就是既有 `historyMessage`(`§4.4`),因此既有读取面(`GET /sessions/web/messages`、握手 `history` 回放、`message_persisted`)**逐字不变**。
+- **破坏性**:无。老客户端按未知键忽略 `id`/`createdAt` 即可;但它会把主动消息**多看到一次**(历史行)—— 这是本版的目的,不是缺陷。
+- **不双份(已验证)**:两条路由在一次推送里**互斥**(`web` 提示只走活跃客户端;其余先 IM 桥、IM 投递失败才降级活跃客户端),而 IM 路由本就把消息镜像回主会话 ⇒ 同一条主动消息在主会话里**恰好一条**。契约细节见 §3.4.5 后的「`proactive_message` 的落库口径」。
+- ⚠️ **诚实边界**:被频率门(`PushGate`)拒绝的推送**不落库**(限流就是「这一条不发」,绝不留下一条它没发过的记录)。
+
+### v0.71(上一版)
+
+**链路图补版 —— 新增只读端点 `GET /api/v1/admin/flow/graph`,让「轮次结束后才打开的页面」也能看到最近一轮的链路图**(依据主人 2026-10-05 第二次报「`/#/admin/flow` 停在暂无流程图数据 + 后台思考行提示该会话还没有 graph_update 快照」;对齐 `design/19 §4` 的 **P3**「多轮历史回放」里的最小一步)。
+
+- **纯增量**:
+ ① **新 admin 只读端点 `GET /api/v1/admin/flow/graph?sessionId=`**(`owner`/`admin`,`?sessionId=` 省略 = 主会话 `web`):返回 `{data:{sessionId,graph}}`,`graph` 与 `graph_update` 帧的 `data` **逐字同形**;该会话还没有图 ⇒ **如实 `null`**(绝不画假图);图源未接线 ⇒ `500 flow_unavailable`(**不假装「这一轮没有图」**)—— **§2.2 B13 / §4.11**。权限矩阵随之更新:`writeAuth` 12 → **13**,总数 64 → **65**。
+ ② §3.4.5 `graph_update` 行补一句「**纯实时、不重放**」并指向本端点。
+ ③ §7.4 新增两条诚实边界:**「最近一轮」≠「你要的那一轮」**(后台思考行上的图可能不是该行那轮;面板必须在图上说明)、**图源是进程内内存**(后端重启后如实回 `null`)。
+- **修的是什么(根因)**:`graph_update` **只走实时帧**,前端图桶 `flowGraphs` **不持久化**(`store/persistOptions.ts` 的 `partialize` 不含它),且此前**没有任何 REST/WS 补拉通路** ⇒ 页面在轮次结束后打开、或刷新过页面,就永远空着。本端点补上唯一缺的那一环。
+- **行为变更**:无(既有端点/帧一字未动)。
+- **破坏性**:无(新端点;`graph_update` 帧形状、`frameReachesAll` 角色门、发布会话范围(裁决 01.4.5)**全部未改**)。
+- ⚠️ **诚实边界**:只回**每会话最近一轮**;**多轮历史回放**(按 turn/时间点取任意一轮)**仍未实现**(`design/19 §4` P3)—— 见 §7.4-18。**`graph_update` 两路互顶**的既有裁决**本版未动**(§7.4-19)。
+
+### v0.70(上一版)
+
+**后台思考行的紧急度精确化 —— `urgency` 默认即精确档位,另加 `urgencyLowerBound` 标记下限**(依据主人 2026-10-05 裁定「要**精确**」;P4 只能给下限,本版把精确值送到两面:管理 REST 与实时帧)。
+
+- **纯增量**:
+ ① `GET /api/v1/admin/backthink` 条目的 `urgency` **默认语义改为该轮实际生效的精确档位**(backthink 在档位判定点随记录落库,含她自报紧急度上抬后的结果)—— **§2.2 B8 / §4.10(b)(c)**。
+ ② 新增**可选** `urgencyLowerBound:true`(只在为 `true` 时出现):标记该 `urgency` 是**旧行的下限推导值**而不是精确档位(旧行 = `urgency` 列出现之前的行,以及从未走到档位判定点的行)。客户端据此区分「精确档位」/「≥ 下限」/「未知」三态。
+ ③ **实时 `backthink` 帧带上记录里的 `urgency`**(精确值)⇒ 本次会话新到的行不必等重开页面就有档位。⚠️ 实时帧仍不带 `urgencyLowerBound` / `quietHoursBypassed`(两者由网关按安静时段窗口推导,不在记录里)—— 更正 §4.10(b) 原「实时帧不带这两个键」的表述。
+ ④ §7.4-17 补「客户端不得把『下限』显示成精确值、不得把缺席显示成『例常』」。面板三态文案与悬停说明见 `frontend/web/src/admin/modules/backthinkUrgency.ts`。
+- **行为变更**:`urgency` 的**读法**变了(v0.68/0.69 一律读作下限;v0.70 默认读作精确值,唯有带 `urgencyLowerBound:true` 才是下限)。取值域(`routine`/`warning`/`critical`)与 `quietHoursBypassed` 语义**一字未改**。
+- **破坏性**:无(新键可选;把 `urgency` 当「下限」显示的老客户端最坏是显示得比事实保守,不会谎报精确值)。
+- ⚠️ **诚实边界**:旧行(本列之前落下的)**没有**精确档位可显示 —— 它们仍走下限额推导并带 `urgencyLowerBound:true`;未知仍是未知(不编值)。见 §4.10(c) / §7.4-17。
+
+### v0.69(上一版)
+
+**`directMsgPerMin` 的 gateway 写入路径修复**(切片 P1-T;缺口 = 面板保存一次渠道配置就把主人手配的「关闭流控(-1)」静默重置回 `0` = 保护性默认 6 条/分)。
+
+- **纯增量 / 缺口修复**:`PUT /api/v1/admin/channels/{platform}/config` 现在**接收并原样落盘**账号顶层 `directMsgPerMin`,`GET /api/v1/admin/channels` 的账号元素也**如实回显**它 —— §2.2 A5 行 / §2.3 C28 注 / §7.2。
+- **行为变更**:无(对不带该键的老客户端,行为与 v0.68 相同)。
+- **破坏性**:无。
+- ⚠️ **诚实边界**:账号级限流字段仍是**值语义**(请求体省略 ⇒ `0` = 未设置),要保住已设的值必须回送 —— 见 §7.3。
+
+### v0.68(上一版)
+
+**管理面「配置事实」只读端点 + 后台思考行的紧急度/穿透字段**(依据主人指正「你多久没把新功能同步管理面板更新了?」与裁决 14;**本版全部已实现、非契约先行**)。
+
+- **纯增量**:
+ ① **新 admin 端点 `GET /api/v1/admin/config-facts`**(`owner`/`admin`,只读、不审计):返回 9 条配置事实(记忆整理 7 键 + `QUIET_START_HOUR`/`QUIET_END_HOUR`)的**进程生效值**(`value`)+ **声明默认值**(`default`)+ `isDefault`,外加 `quietHours{startHour,endHour,disabled,label}` 的窗口解释 —— **§2.2 B12 / §4.10(a)**。**不含任何凭据值**(连凭据类键的「是否已配置」都不回)。权限矩阵随之更新:`writeAuth` 11 → **12**、总数 63 → **64**。
+ ② `GET /api/v1/admin/backthink` 的条目增 2 个**可选**字段:`urgency`(`routine`/`warning`/`critical`)与 `quietHoursBypassed`(`true` 才出现)—— **§2.2 B8 / §4.10(b)**。语义:`urgency` 是**存档可证的下限**(`warning` = 至少 `high`),`quietHoursBypassed` = 「这轮在安静时段里仍被放行发出去」。
+ ③ §5 登记 9 个键的默认值与语义(此前只以 `THINK_*`/`QUIET_*` 概括)。
+- **行为变更**:无(既有两个端点的既有键一字未改;新字段都是可选键,老客户端不读即零影响)。
+- **破坏性**:无。
+- ⚠️ **诚实边界**:后台思考的**精确**档位(`high` 还是 `critical`)在存档里**已丢失**(`backthink.Record` 不带重要度,推送成功的行其 `detail` 被投递说明覆盖)⇒ 本版只下发**可证下限**,窗口外的轮次干脆不下发(面板显示「未知」),**绝不猜**。见 §4.10(c) / §7.4-17。
+
+### v0.67(上一版)
+
+**三批已在设计文档定稿的新增面一次性登记**:语音消息(播放端点 + 消息 DTO `voice` + 音频副本口径)、非消息事件查询面、账号在线状态(依据 `design/24 §10.11`、`design/26 §4.5` / `§2.6`)。登记时三批**都还没有实现**;其中**账号在线状态**(`52dac45`)与**非消息事件查询面**(本切片 E6)**已落地**(见下)。
+
+> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态**(见 §2.4(b))与**非消息事件查询面**(见 §2.4(a))**已落地**;**语音消息**当时未实现,那一批的形状**取自设计文档而非代码核对** —— **v0.79 已把用户可见面落地**(切片 L45,见 §6 v0.79 ③ / §4.5 / §7.2),**残留缺口**(WS 实时帧/握手回放不带 `voice`;AMR 的那一项**已由 v0.80 修复**)仍按「未确证/未实现」读。
+
+- **纯增量**:
+ ① 新端点 `GET /api/v1/media/{name}`(用户侧,**owner/admin 必须 Bearer**):`200` 字节流 / `206 Partial Content` / `304 Not Modified`;`Content-Type: audio/*`;`Content-Disposition: inline; filename=""`;`nosniff`;`Accept-Ranges: bytes`;`Cache-Control: private, max-age=86400, immutable`;支持 `Range` 与 `HEAD`;`404 media_not_found`;`401`/`403` 由中间件给出。⚠️ **`416` 是标准库直写的非 JSON 响应**(见 §0.3、§4.1、§4.5)。
+ ② 消息 DTO 增**可选** `voice` 对象(`omitempty`;**非语音消息键缺席**):`{source,status,name?,mime?,size?,durationMs?,url?}`;`source` ∈ `asr`/`platform`,`status` ∈ `transcribed`/`empty`/`failed`,`name`/`mime`/`size`/`url` **四者同生共死**(有副本时才在);`url` 是服务端相对路径 `/api/v1/media/`。**🎤 判据 = `voice` 键存在**。⚠️ **v0.79 校正(实测覆盖)**:真正会带 `voice` 的读取面**只有** `GET /sessions/{id}/messages`、`GET /admin/channels[/{platform}]/messages`、`PATCH /messages/{id}` 响应与 `message_edited`;**`message_persisted` / `channel_persisted` 与握手 `history` 回放不带**(见 §3.2、§3.4.4、§4.4 覆盖率的「尚未填充」)。
+ ③ 新错误码 **`media_not_found`(404)** → 加进 §4.1 的 404 行(与 `document_not_found` 并列;**刻意不复用**它,否则客户端分不清缺的是文档还是语音)。
+ ④ 新错误形态 **`416`**(**非信封**,标准库直写)→ §0.3 / §4.1 / §4.5。
+ ⑤ 新 admin 端点 **`GET /api/v1/admin/channels/events`**(非消息事件列表,倒序;`?platform=&tier=&category=&channelId=&since=&until=&limit=&offset=`;响应 `{data:{events:[…],total}}`;`since`/`until` 按 UTC ISO-8601 定宽字符串**字典序**比较、**跨度上限 90 天**;不含正文、不含媒体 URL、不含任何凭据 / `flag`)—— §2.4(a)。**已落地**(`owner|admin` 档;元素键名 camelCase,逐字见 §2.4(a);未给窗口 ⇒ 不注入窗口)。
+ ⑥ 新 admin 端点 **`GET /api/v1/admin/channels/events/counters`**(逐类计数汇总 `{data:{approx:true,counters:[{platform,accountId,tier,category,platformType,count,firstSeenAt,lastSeenAt}]}}`)—— §2.4(a);未接线存储 ⇒ **`500 channel_events_unavailable`**(新增错误码,**绝不回全 0 假汇总**)。**已落地**(`approx` 与 `counters` 同级;`platform` 未知 ⇒ `400 bad_request`)。
+ ⑦ `GET /api/v1/admin/channels` 的 `accounts[]` 增**可选** `accountOnline`(**账号在平台侧是否在线**,与 `connected`(适配器/WS 是否连着)**是两件事**;**键缺席 = 状态未知 ≠ 离线**)—— §2.4(b)。**已落地**(管理面按可选接口断言读 `AccountStatus()`;`known=false` 或适配器无该状态面 ⇒ 键缺席)。
+ ⑧ 新 env 键 3 个:`CHANNEL_EVENT_RETENTION`(`720h`)/ `CHANNEL_EVENT_GC_INTERVAL`(`1h`)/ `CHANNEL_EVENT_COUNTER_FLUSH`(`30s`)—— 已在 §5 登记(本切片**不新增**语音相关 env 键;语音上限/保留期复用既有常量与 `DOCUMENT_*`)。
+ ⑨ **WS 协议零变化**:`media` 是 REST 端点;无新帧、无新上行字段。
+- **行为变更**:
+ ⑩ **QQ 语音消息的 `content` 不再含 `[语音]` 占位标记**(旧行为 → 移除):今天从正文里认 `[语音]` 的客户端**必须**改为看结构化字段(**`voice` 键存在 = 语音消息**)。⚠️ **这是本版唯一会打到老客户端的地方**(见 §7.2)。
+ ⑪ `GET /api/v1/admin/system/storage` 的 `documents` 段(件数 / 字节)**自此包含语音副本**(音频副本与文档**同表**:`SELECT COUNT(*), SUM(size) FROM documents`)—— **形状不变**,变的只是**口径**(面板文案宜标注「含语音副本」)。
+- **破坏性**:无(新端点、新可选字段、新错误码都是增量;`documents` / `/upload` / `/files` / `/audio/*` 的既有端点**一字未改**;WS 零变化)。
+
+### v0.66(上一版)
+
+**并行帧层上线 + 消息回合标识落库 + 渠道按账号操作;并修正 v0.65 的两处过期/写错的陈述**(依据 design/15 §2.5、design/08 §4;提交 `c0dc435` / `d64b26d` + `d804c4e` / `42279b7` / `a918819` / `679cee0`)。
+
+- **纯增量**:
+ ① 新下行帧 **`turn_begin`**(owner+admin,**不进** `frameReachesAll` 白名单):帧壳 `{type,sessionId,turnId}`、data `{kind,messageIds,leg,activeTurns}`,**每起一路(含续跑路)恰好一帧**(`c0dc435`)。
+ ② **`steer_ack` 增帧壳 `turnId`**(data `{messageId,atStep}` 不变)(`c0dc435`)。
+ ③ **上行 `interrupt.turnId`**:仅 `scope:"current"` 生效(其它 scope 按契约忽略);目标路不存在 ⇒ **定向** `{type:"error",data:{code:"bad_request"}}`(复用既有 code),不广播、不动会话(`c0dc435`)。
+ ④ 消息 DTO 增**可选** `turnId`(`omitempty`,空则键省略;同一路的 user 与 assistant 同值)—— 一次覆盖 `GET /sessions/{id}/messages`、`PATCH /messages/{id}` 响应、`GET /admin/channels[/{platform}]/messages`、WS `message_persisted` / `channel_persisted` / `message_edited`、握手 `history` 回放(`d64b26d` + `d804c4e`;迁移 `0018`)。
+ ⑤ `POST /admin/channels/open|close` 请求体增**可选** `accountId`;未知 id ⇒ `404 account_not_found`,已配置未注册 ⇒ `409 account_not_registered`;响应增 `resolved` / `accountId`(`42279b7`)。
+ ⑥ `DELETE /admin/channels/{platform}/config` 增**显式** `?empty=1`(与 `?id=` 互斥 ⇒ `400`;无可删 ⇒ `404`);响应增 `empty` / `removed`(`42279b7`)。
+ ⑦ `GET /admin/channels` 的 `accounts[]` 增**可选** `connected`(键缺席 = 状态未知 ≠ 未连接)(`42279b7`)。
+ ⑧ 新 env 键 `VOICE_STT_TIMEOUT_SEC`(默认 `20`;**只约束 IM 入站语音**,Web `voice_input` 不受其约束)(`a918819`)—— 已在 §5 登记。
+- **行为变更**:
+ ⑨ **IM 用户消息现在带 `documents`**(同一 DTO、无新字段;0 附件仍**省略**该键)(`679cee0`)—— v0.65 曾预告,本次随该提交真正上线。
+ ⑩ `turn_begin` 的**帧序**:`session_busy`/`pending_drain` → `message_persisted`(user) → `turn_begin`(`messageIds` 必须是已落库的权威 id,故只能在落库之后发)(`c0dc435`)。
+ ⑪ **不带 `accountId` 的 open/close 目标现在是确定性的**:按 `channels.json` 有效配置顺序第一条(此前走注册表 map 顺序,多账号平台上每次可能落到不同账号)(`42279b7`)。
+ ⑫ `DELETE .../config?empty=1` 由「被当成 id 缺失 ⇒ `400`」变为**真的删除无 id 条目**(显式化;`?id=` / `?empty=` 语义边界见 §2.3 注)(`42279b7`)。
+- **破坏性**:无(新帧对新客户端可见、老客户端忽略即可;所有新增字段皆可选;无既有字段被移除或改义)。
+- ⚠️ **本版更正 v0.65 的两处陈述(文档写错,不是代码变更)**:
+ - **`thinking` 口径**:本文档曾写「历史思维链只有 `GET /sessions/{id}/messages`(owner/admin)返回」—— **与实现不符**。`history.Message.Thinking` **全仓没有任何写入者**(`loop.Result` 无该字段、`finalizeAnthropicStream` 直接丢弃思考文本)⇒ 该键在**所有**读取面**恒缺席**;思维链的**唯一出口**是实时 `thinking.*` 帧(owner/admin)。已在 §3.4.4、§4.4 按实测改写。
+ - **v0.65 §7 的「未实现」清单**:`turn_begin` / `steer_ack.turnId` / `interrupt.turnId` / `chat_messages.turn_id` / 写类工具文件级互斥 —— **现已全部实现并上线**(`c0dc435`、`d64b26d`+`d804c4e`、`354b08a`),§7 已相应改写。
+- ⚠️ **v0.66 补充说明(描述性更正,不动形状/阈值,不升版本号)**:**`chat.image` 的体积判据是「字符串长度」而不是「解码后字节」** —— 旧措辞「内联上限 = 解码后 5 MiB」只对 **base64 / `data:` 形态**成立,且它本身是**换算口径**(`len(image) <= 6990572` 字符,见 §4.5)。**URL 形态不触发 `image_too_large`**(URL 串约 60 字符,必然通过):其天花板在视觉取回层的 **50 MiB**(`io.LimitReader`,**超出静默截断**、不报错);**`POST /api/v1/upload` 自身是 5 MiB 字节硬限**(`413 payload_too_large`)。四处已按实现写清(§4.5 图片、§4.6 响应面、§5 环境变量表、U8 行 + 错误码表)。**无端点 / 帧 / 字段形状变更,无默认值变更**。
+
+### v0.65
+
+**忙时并行回合基础管线(I1′ 有条件放宽)+ 六条登记**(依据 design/15 §2.5,主人 2026-10-05 裁决 B)。
+
+- **纯增量**:① 上行 `chat.parallel?:bool`(WS `chat` 帧 + REST 三入口同形);② `session_busy`/`session_idle`/`pending_drain`/`session_interrupted` 的 `data` 增 `activeTurns:int`;③ `session_interrupted.data` 增 `cancelledTurns:int`;④ `pending_ack.data` 增**可选** `reason:"parallel_limit"`;⑤ REST 两条新应答形状(同步 `202 {parallel:true,turnId}` / `stream:true` 的 `200 {stream:true,parallel:true,turnId}`);⑥ 新 env 键 `VOICE_STT_TIMEOUT_SEC`(默认 20s)。
+- **行为变更**:⑦ IM 入站附件引用**真的写进落库的用户消息**(`documents` 列,同一 DTO、无新字段);⑧ IM 各渠道语音进上下文(云端 ASR 转写为文本,失败落一行中文诚实提示);⑨ `session_idle` 时点收紧为「所有在途路收尾且 pending 空」(**仅并行时生效**)。
+- **破坏性**:无(省略 `parallel` / 不读新键的客户端行为逐字不变)。
+- ⚠️ **回填**:上条 ⑦(IM 附件引用落库)与 ⑥(`VOICE_STT_TIMEOUT_SEC`)在 v0.65 冻结时**尚未提交**,已分别在 `679cee0` / `a918819` 落地,见 v0.66。
+
+### v0.64
+
+**LLM 用量落库 + 只读汇总端点 + 面板用量小卡**。
+
+- **纯增量**:新端点 `GET /api/v1/admin/llm/usage`(owner/admin 读)、新迁移 `0017_llm_usage`、新 env 键 `LLM_USAGE_RETENTION`(默认 `720h`,`≤0` = 关闭清理)。
+- **行为变更 / 破坏性**:无(`model` 层新增的是**可选** sink,不注入时行为逐字不变)。
+- ⚠️ v0.64 曾写「`llm_usage.session_id` 恒为 `NULL`」,**已由 v0.65 更正**:loop 已把会话 id 播种进运行 ctx 并落库(空串仍映射 `NULL`);但 `GET /admin/llm/usage` **仍不按 `session_id` 过滤/分组**,响应形状不变。
+
+### v0.63
+
+**客户端登记两条落地:`documents[]` 增可选 `reason` + `GET /sessions/{id}/messages` 对不存在会话回 `404 session_not_found`**。
+
+- **纯增量**:① `documents[]` 元素增**可选** `reason`(`omitempty`,只在 `status:"unreadable"` 时下发)。
+- **行为变更**:② `GET /api/v1/sessions/{id}/messages` 对**不存在/已删除**会话由 `200 {"data":[]}` 改为 **`404 session_not_found`**(客户端须处理 404)。判据是会话注册表 `chat_sessions`:隔离 `sess-*` 也回 `404`(其消息走 `/admin/channels/{platform}/messages`)。
+- **破坏性**:无。
+
+### v0.62
+
+**面板「重置为 env 默认」动作**:新增 owner-only `POST /api/v1/admin/llm/reset-from-env`(body 必须 `{"confirm":true}`)。
+
+- **纯增量**:新端点 + 新审计动作 `llm.resetFromEnv` + 2 个新错误码(`env_defaults_unreadable` / `models_unreadable`),复用既有 `llm_persist_failed`/`llm_reload_failed`/`llm_unavailable`。
+- **行为变更 / 破坏性**:无。
+
+### v0.61
+
+**PDF 文本层抽取:白名单第 23 项 `.pdf` + 类型裁决改为 `(扩展名, 嗅探)` 成对裁决**。
+
+- **行为变更**:① `.pdf` 由「一律 `415`」变为「可上传 + 可读取 + 文本层可抽取」;② 类型裁决收紧为成对判定(HTML/纯文本改名 `.pdf` ⇒ `415`;PDF 改名 `.txt` ⇒ `415`)。
+- **纯增量**:白名单 22 → 23 项(唯一新增 `.pdf`),其余 22 个文本类判定与响应一字未变。
+- **破坏性**:无。
+
+### v0.60
+
+**管理侧只读「存储状态」端点 + 面板存储卡接真数据**:新增 `GET /api/v1/admin/system/storage`(**owner-only**、只读、无审计动作)→ `{data:{main,documents,vector}}`。
+
+- **纯增量**:新端点 + 前端新行为;既有端点/帧/字段形状一字未动。
+- **行为变更 / 破坏性**:无。
+
+> **更早版本(v0.28 → v0.59)**:见归档 `Docs/archive/api-client.v0.65.md` §5。
+> **未实现的契约变更记录**:归档 §5 里 v0.56–v0.59 的多条「未做/诚实边界」在 §7 重新登记。
+
+---
+
+## 7. 在飞切片与「未实现 / 未确证」
+
+> 本节是**诚实边界**:让客户端**不要以为已有**、也不要**误以为没有**。
+
+### 7.1 在飞切片
+
+**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)与非消息事件查询面都已落地**(见 §7.2);**语音消息的用户可见面已由 v0.79 落地**(仍残留 WS 帧/握手回放不带 `voice`;AMR 不落副本**已由 v0.80 修复** —— 见 §7.2 / §7.4-29),**不在本表**,逐条见 §7.2。
+
+| 项 | 状态 | 冻结位置 |
+|---|---|---|
+| 新下行帧 **`turn_begin`** | **已上线**(`c0dc435`) | §3.4.3;`session_events.go`、`session_parallel.go` |
+| **`steer_ack` 的帧壳 `turnId`** | **已上线**(`c0dc435`) | §3.4.3;`session_steer.go`(`emitTurnState`) |
+| **`interrupt.turnId`** | **已上线**(`c0dc435`) | §4.8;`ws_hub_turn.go`、`session_interrupt.go` |
+| **消息 DTO `turnId`**(含 `chat_messages.turn_id` 迁移 `0018`) | **已上线**(`d64b26d` + `d804c4e`) | §4.4;`history.go`、`session_turn_id.go` |
+| **写类工具文件级互斥**(读共享 / 读写与写写互斥,被占住回 `ErrFileBusy` 文案给模型) | **已上线**(`354b08a`、`ef3dcb6`)—— **纯服务端行为,无客户端可见契约面** | `internal/runtime/tool/{fileclaims.go,legfile.go,tool.go}`、`loop/tools.go` |
+| **保留期清扫的「可分辨键」`retentionBoundaryReached`**(v0.81 新增,切片 X1) | ✅ **已上线(`2bf3b53`)** | 作用:把「更早的轮次**被保留期清扫**」与「分页还有更多 / 本来就没有更早的轮次」**分开**(`truncated` 只答「本次是不是全部」)。形状 / 判据 / 三个盲区见 **§4.11.1(a)(b)-3**;`round` 跨重启单调的同期修复见 **§4.11.1(e)**。⚠️ v0.81 登记时它**仍在工作树**(该版已如实标注);**现已提交** ⇒ 按 `git show 2bf3b53` 复核键名与判据 |
+| **她自排的入口工具 `request_think`**(→ 使 `GET /admin/backthink` 的 `trigger="self_wake"` 与 4 条 `backthink.self_wake.*` 审计动词**真的可能出现**) | ✅ **已接线(切片 X2 `2d211c2`)** —— v0.81 登记时的「🔴 未落地」**已作废** | 判定侧 C1-1 `94da6bc` + 自排本体 C1-2 `ed50053`(窗口上限 + 容量 1 待跑槽 + 4 条审计)+ **入口工具 `request_think` 已注册**(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`,`internal/services/backthink/thinktool.go`)⇒ **生产里会真的产生自排轮**;启动日志自证 `self wake tool registered tool=request_think`。形状见 §4.10(d);⚠️ 自排轮仍要过既有 `decide` 级联 + `PushGate`,**不保证每轮都有**。**读它用了多少额度的出口**见 **§4.10(e-2) / §2.2 B19** |
+| **`tool_execution.end` 的 `resultSummary`**(v0.82 新增,切片 Y1 `215eedf`;**「失败也带」在工作树**) | 🟡 **已实现;「失败也带」登记时仍在工作树、待提交** | 形状与缺席语义见 **§3.4.2**。⚠️ 提交前跑着的后端里**没有**该键;提交后请按 `git show` 复核「失败路径给 error 摘要」这条口径(`internal/runtime/loop/tool_frame_summary.go` 的 `toolEndFrameSummary`) |
+| **`browser` 面板与截图闭环的组合根挂点**(v0.82 新增,切片 Y4 `e0a6bca`) | 🔴 **登记时未接线**(代码已落库,两个生产挂点在工作树) | 声明与六个动作见 **§4.14(d)**;`wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)` 与 `wire_runtime.go` 的 `TransformContext: a.browserShotTransform(visionResolver),` 是**登记时**才进工作树的(删掉都**零症状**,各有源码级用例盯着)。🔴 **在跑的那份产物里没有它们**(真机已核:启动日志全篇 0 条 `browser panel declared`)⇒ 那一刻 `pluginIds == ["web-search"]`、`browser` 动作全 `404 action_not_declared`、`turnAttachment` 照报 `queued` 但图不进上下文 |
+
+> ⚠️ **工作树另有未提交改动(v0.82 登记时的实测状态)**:**与本版两个面相关**的在途文件共 **5 个**(此外工作树还有同波其它切片的在途改动,与本契约无关、不影响本文档):
+> - **① `resultSummary` 的「失败也带」**:`internal/runtime/loop/tool_frame_summary.go` + `..._test.go`(已按「登记形状 + 明标未提交」进上表);
+> - **③ `browser` 的两个生产挂点**:`cmd/cyrene/wire_runtime.go`、`cmd/cyrene/wire_frontend.go`、`cmd/cyrene/wire_browser_context_test.go`(同样已进上表)。
+>
+> 其余在途项(若有)**尚未冻结、本表不登记** ⇒ 客户端**不要**按当前工作树形态编程。⚠️ v0.81 时代本注举的项(X1 的时间轴两项、`request_think`、浏览器 SSRF 收口、`capacity` / 面板 `Backthink.tsx`)**均已提交**(`2bf3b53` / `2d211c2` / `a820d99`),不再是在途项。
+> 归档版 v0.65 §J 曾把上表前四项列为「未实现」——那是**归档时的代码状态**,**已过期**。
+
+### 7.2 确认为「未实现」(别当已有)
+
+| 项 | 现状 |
+|---|---|
+| **✅ v0.67 登记的「语音消息面」**(`GET /api/v1/media/{name}` + 消息 DTO `voice` + 入站语音音频副本落盘) | **用户可见面已实现(v0.79,切片 L45)**:端点 / 字段 / 副本口径见 §1.2 U19、§4.4、§4.5。落地:`documents.Store.SaveAudio`(音频白名单 / 内容寻址 / 0600)、`history.Store.UpdateMessageVoice` + `VoiceColumns`(`chat_messages.voice` 列读写两条路)、网关 DTO `voice`(含唯一派生 `url`)与 `GET /api/v1/media/{name}`。🔴 **残留缺口(如实)**:① **`message_persisted` / `channel_persisted` 帧与握手 `history` 回放仍不带 `voice`**(需给 `history.Message` 加字段,**未做**;且 `message_persisted` 在落库那一刻发出,天生拿不到落库后才回填的值)⇒ **客户端不要在实时帧/握手回放上等 `voice`**;② ✅ **已修(v0.80,切片 W4 / 裁决 #11)**:组合根**已注入 `VoiceTranscoder`** ⇒ **AMR 也落副本**(落盘前转成 `.wav`,`mime: audio/wav`);`transcode_not_wired` 只剩「引擎未装配 / 构造失败」一档(**生产装配下不再出现**);③ **消息/会话被删除时音频副本引用计数不被释放**(`refs.go` 只认 `document` 列 ⇒ `ref_count` 偏高,后果是**晚回收**,绝不早删);④ **生产端到端未跑**(未重启后端)。⚠️ 同批的**口径变更**(`/admin/system/storage` 的 `documents` 段含语音副本)仍**尚未生效**;**已生效**的是 IM 语音「不可转写格式」正文提示改口径(**形状不变**,见 §4.5)。权威设计 `design/24 §10` |
+| **✅ v0.67 登记的「非消息事件查询面」**(`GET /admin/channels/events`、`GET /admin/channels/events/counters`) | **已实现**(v0.67 登记项本切片落地):形状与错误码见 §2.4(a)(已按代码核对:元素键名 camelCase、`approx` 与 `counters` 同级、`400 bad_request`、`owner\|admin` 档);权威设计 `design/26 §4.5`。未接线存储 ⇒ `500 channel_events_unavailable`。⚠️ 本行留在此表仅为与上一版对照,下版改文档时应移入 §7.1 在飞切片表并回填 §2.1/§2.2 的端点计数 |
+| **✅ v0.67 登记的「账号在线状态」**(`GET /admin/channels` 的 `accounts[].accountOnline`) | **已实现**(v0.67 登记项本次落地;落地前基线 `f91a83f`,本切片 commit 由主会话补记):形状见 §2.4(b)。管理面按**可选接口断言**读 `AccountStatus()`(`plugins/channels/qq_obv11/probe.go`,`a79fe96`);适配器**没有**该状态面、或实现了但 `known=false` ⇒ **键缺席**(未知 ≠ 离线,绝不用 `false` 冒充)。当前只有 `qq-obv11` 会带该键。⚠️ 本行留在此表仅为与上一版对照,下版改文档时应移入 §7.1 在飞切片表 |
+| **✅ P1-T:`directMsgPerMin` 的 gateway 写入路径**(`GET /admin/channels` 的 `accounts[].directMsgPerMin` + `PUT .../config` 的顶层 `directMsgPerMin`) | **已实现**(v0.69):缺口 = gateway 面没让该字段通过 ⇒ **面板保存一次渠道配置就把主人手配的「关闭流控(-1)」静默重置回 `0` = 保护性默认 6 条/分**。现 5 处透传(`AdminChannelAccount` DTO / `channelConfigRequest` / `config.ChannelAccount` 拷贝 / `adminChannelAccountOf` / 微信扫码重建账号)。形状见 §2.2 A5 与 §2.3 C28 注 |
+| **✅ 「工具回看面」**(消息 DTO `toolCalls[]`) | **已实现(v0.78 落地,v0.79 起覆盖全部编排回合)**(切片 CODE-TC + CODE-TC-2):形状、缺席语义与**覆盖率**见 **§4.4**;读侧依赖 = 迁移 `0023` 的 `flow_graphs.lane_turn_id`(等值索引)+ `FlowGraphStore.GraphsByTurnID`(一页一次 `IN` 批量查,禁 N+1)。✅ **v0.79**:编排器驱动的**每一路**(含普通单路回合与队首续跑路)的图都带权威 `turnId` ⇒ **普通单路聊天与队首续跑回复现在也能回看工具**。⚠️ **仍缺席**:后台思考(`sess-backthink`)、未接编排器的调用方、v0.79 之前的单路历史行。⚠️ 单路在 **HTTP 出口**的端到端**未由本切片确证**(写侧已验,gateway 读侧待补)。见 §7.4-23 / §7.4-25 |
+| **✅ v0.76 登记的「未读面」**(`message_persisted.data.unreadCount`) | **已实现**(本切片):形状、缺席语义与三条缺席路径见 **§4.13**,帧表见 §3.4.4。⚠️ `GET /api/v1/sessions` 的同名键**自 D4 起就已在**(`historySessionSummary.UnreadCount`),本片**未改**它 —— 所以「加同键」这句对列表面其实是**既有事实**,不是本版新增 |
+| **`graph_update` 按路分桶** | **✅ 已实现(v0.77,切片 FV2)**:帧 `data` 新增每路的 `turnId` 与 `round`,图按 `{会话}#{路}` 分桶 ⇒ **每一路各发自己的帧、各落一行**(不再互相顶掉);管理面 `GET /admin/flow/graph` 新增 `lanes[]`。形状与缺席语义见 §3.4.5 / §4.11(a)(d),登记见 §7.4-19。⚠️ 真并行**未做真机端到端**(见 §7.4-24) |
+| **第二路进行态 system 摘要**(`WithParallelNotice`) | **未实现**:全仓无该符号;`session_parallel.go` 明示留待后续切片 |
+| **env `CHAT_MAX_PARALLEL_TURNS`** | **未接线**:`Deps.MaxParallelTurns` 是构造期旋钮,但组合根未从任何 env 读它 ⇒ **生产恒为默认 2** |
+| **`tool_execution.update` 的生产者** | wire 映射已实现,但工具分发器暂无中途进度上报源(全仓无 `StreamKindToolExecutionUpdate` 的生产者)⇒ 实际**暂无生产者**(`start`/`end` 真实发出) |
+| **`chat_messages.turn_id` 上的 IM 行** | IM 渠道桥**不走**网关编排器 ⇒ 其行 `turn_id` 留空属**预期**(契约里该键缺席),不是缺陷 |
+| **记忆整理候选的 apply / reject / revert + 候选列表 + 运行历史**(`design/23 §11`) | ⚠️ **已部分实现(v0.83,切片 TIDY-1)** —— v0.75 只加了「试算」(B14,**不改任何记忆**);v0.83 补上了**执行面**:`GET /admin/memories/candidates`(B20,只按 `status` 筛)、`POST .../candidates/{id}/apply`(B21)、`POST .../candidates/apply`(**批量**,B22)、`POST .../candidates/{id}/revert`(B23)、`POST /admin/memories/tidy/run`(**手动真跑**,B24)**都已实现并推库**。🔴 **仍缺三样(别假定它们存在)**:① **`reject`**(把候选标成「不采纳」而不应用)**没有端点**;② **运行历史读侧** `GET /admin/memories/tidy/runs` **没有**(真跑的留痕行**已经落库**在 `memory_tidy_runs`,只是没有读口);③ 候选列表**不能按 `runId` 筛**(只能按 `status`)—— 按 run 只能走**应用**那一档(B22 的 `runId`)。⚠️ **别与 v0.79 的 `POST /admin/memories/{id}/revert` 混淆**:后者是**入库前审查(D3)软合并**的回滚(按**记忆 id**,§2.2 B17),与「整理候选」(按**候选 id**)是**两件事** |
+| **`GET /api/v1/admin/memories` 的整理相关字段**(`kind` / `tags` / `confidence` / `createdAt` / `updatedAt` / `sourceConversation` / `status` / `mergedInto` / `mergeCount`) | **未实现**:该端点当前仍只回 `{id,scope,text}`(`design/23 §11` 登记的读侧缺口)。v0.75 **未**改它的形状 ⇒ 面板暂时无法在这一页按 kind/status 过滤(试算端点 B14 的候选自带这些分区键与 kind)。⚠️ **v0.83 起服务端已按 status 过滤**(四条列表读路径只回 `active`)⇒ 被软合并 / 被归档的记忆**不再出现在这一页**;但**字段面仍未补齐** ⇒ 面板在这一页**看不到** `status`/`mergedInto`/`mergeCount`,也就**无法显示「哪些已被合并」**(那要看 **B20 候选列表**或 **B16 审查留痕**) |
+| **✅ 记忆「入库前审查」的面板面**(`GET /api/v1/admin/memories/reviews`、`POST /api/v1/admin/memories/{id}/revert`) | **已实现(v0.79,切片 L45;v0.77 登记时无 HTTP 入口)**:两条都走 `writeAuth`(owner\|admin),形状见 §2.2 B16/B17。⚠️ **残留**:① 留痕列表**只有 `limit`、没有分页游标**(无 `before`/`offset`);② `memory_ingest_reviews` **无 TTL / GC**;③ 回滚**只回滚「被吸收行」这一侧**(不恢复存活行被抬高的 `confidence`);④ 门与 `design/23 §11` 表格的 owner-only **有差异**(待裁决)。同样是**新审计动词** `memory.review.revert`(`design/23 §16.4` 表里还没有它,需设计侧补记) |
+| **✅ v0.78 插件动作的「执行器」**(`POST /admin/plugins/{id}/actions/{action}` 的动作本体) | **已接线(v0.79,切片 L1/PLG-2 执行面接线)**:插件管理器新增**动作处理器登记面**(`Manager.RegisterActions`;单一全局 `SetExecutor` 已删除),组合根在 `wirePanelHub` 里登记内置 `web-search` 的四个动作 + 候选来源 `plugin.providers` ⇒ **这些动作真执行**(真读写可变源、探针真发一次 HTTP)。**`501 action_not_implemented` 只在「已声明但宿主未登记处理器」时出现**(不再是兜底档)。✅ **v0.80(切片 W3)**:面板保存后**热重载**(重建工具 + 原子替换注册表条目,**下一轮生效、不需重启**;会让后端下次起不来的配置 ⇒ **按字节回滚** + `409 config_unstartable`);面板**已可配主 provider / 降级链**(`primary` / `fallback[]`)。⚠️ **残留**:**探针只用 `httptest` 假上游**(未对真实上游连通);**启用态不跨重启持久化**;**真机面板端到端未跑**。见 §4.14(b)(c) / §7.4-26 / §7.4-27 |
+| **🔴 v0.78 热装插件的面板声明来源**(`plugins//manifest.json` 读取) | **未实现**(切片 PLG-2 / PLG-1):目录能键热装插件、生命周期已接,但**没人喂声明** ⇒ 热装插件在 `GET /admin/panels` 的 `pluginIds` / `groups` 里**恒为「未声明管理页面」**。有声明的是**内置**插件(组合根装配期 `Declare`):`web-search`(v0.78 起)、**`browser` / `settings`(v0.82 起,切片 Y4)** |
+| **🔴 v0.82 `browser` 面板与 B3 截图闭环的「生产挂点」** | **登记时未接线**(切片 Y4 `e0a6bca`):声明与六个动作的代码已落库(§4.14(d)),但**组合根两行**(`wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)`、`wire_runtime.go` 的 `TransformContext`)**在工作树、尚未提交** ⇒ 提交前:真机上 `pluginIds == ["web-search"]`、`POST /admin/plugins/browser/actions/*` **全部 `404 action_not_declared`**、`browser` 工具的 `screenshot.turnAttachment` 照报 `queued` 而图**不会**进上下文。**客户端不要**按未提交形态编程(同 §7.1 那一行) |
+| **🔴 v0.82 `browser` 面板 / `resultSummary` 的「真机端到端」** | **未确证**:两者都只有测试进程内的证据(`httptest` / 真 hub / 真 loop),`:10942` 上的发布产物是旧构建 ⇒ 真机端到端一律未跑。见 **§7.4-30 / §7.4-31** |
+| **`PUT/POST /api/v1/admin/plugins/{id}/enable\|disable`** | **未实现**(切片 PLG-2):`Manager.SetEnabled` 已实现并有测试,但**没有 HTTP 面** ⇒ 客户端**不得假定**能经 REST 启停插件(`panels_changed` 的 `enable`/`disable` 两种 `reason` 因此当前**不会**在运行期出现)。⚠️ 且启用态**不跨重启持久化**(`PluginConfig` 无 `enabled` 字段,`SetEnabled(false)` 只在进程内生效,重启按「已安装」恢复为启用) |
+| **`panels_changed` 帧对 `viewer` 可见** | **未实现(刻意)**:该帧**未**登记进 `frameReachesAll` 白名单 ⇒ 按 fail-closed 默认档**只发 owner/admin**,**viewer 收不到**(须自行刷新/轮询重拉 A13)。放开需先改 `ws_frame_scope.go` + 在 `design/11 §7.1` 登记理由(**不在本版**) |
+| **插件动作审计的持久化粒度** | **未含 pluginId / action**(切片 PLG-2):持久化审计记录沿用既有 `AuditStore.Append(ctx, who, what, result, ts)` 四字段 ⇒ 只落 `what="plugin.action"`;插件 id 与动作名只在**结构化日志行**里。要进持久化记录需改 `AuditStore` 签名(**不在本版**) |
+| **🔴 v0.78 渠道事件实时面的「其余全部事件」** | **未接线**(切片 E7):覆盖桥接层写入的**全部**渠道事件的 `events.PublishingStore` 装饰器**已实现但组合根未接**(`grep` 全仓无 `NewPublishingStore` 的生产调用点)⇒ 目前**只有账号状态迁移这一路**会产生 `channel_event` 帧;其余渠道事件仍**只能靠 REST 轮询**。接线只需组合根两处(`events.NewPublishingStore(...)` + 把 `wire_channels.go` 的 `EventStore` 换成它),**⚠️ 只装饰这一处**(网关侧 `Deps.EventStore` **不要**也换,否则账号状态行会被推两次) |
+| **🔴 v0.78 `GET /admin/channels` 的 `accounts[]` 每账号凭据三态** | **未实现**(切片 CODE-F3):`authState`/`authDetail` **只**落在 `GET /admin/channels/{platform}/login-status`(§2.4(c));**每账号**版本需要给 `AdminChannelAccount` 加两字段 + 一个合并器 + 一次取数(收集器 `liveAccountAuthStates()` 已实现并有单测,**无调用方**)⇒ **客户端不得假定 A5 的 `accounts[]` 会带这两个键** |
+
+> ⚠️ **本表(§7.2)的两条口径**:① 标 **✅** 的行(如 v0.67 的两项、`directMsgPerMin`、`graph_update` 按路分桶、`toolCalls[]`、`unreadCount`)**留在表里仅为与上一版对照**,它们**已经是可用契约面**,见各行指向的章节;② 标 **🔴** 的行**不要当已有**。
+
+### 7.3 诚实边界(既有事实)
+
+- `subagent_result` **不落库**:父会话转录里没有新消息、**重连回放不补发**。
+- **`proactive_message`(v0.72 起)落库**:成为主会话 `web` 的一条 `assistant` 行(见 §3.4.5 后的「落库口径」)。**仍未落库的情形**(都是如实、预期的):① 被 `PushGate`(频率门 / 冷却 / 按风格窗口)或安静时段挡下的推送 —— 「这一条不发」就没有记录,**绝不留下一条它没发过的消息**;② 后端未接线历史存储(`HistoryStore: nil`,仅测试与异常部署)时只发帧不写库,进程重启即丢。
+- **`task_done` / `automation_result` / `subagent_result` 不是会话消息**:它们**不进消息 store、不是气泡**(后端也没有把它们写成 `chat_messages` 行;`subagent_result` 更是**错过即永久丢**)。客户端应把它们落到自己的可观测面(Web 端落在本端 WS 事件回显 + 任务日志)。
+- `GET /api/v1/files/{name}` **无鉴权**(能力 URL);文档读侧相反、**必须 Bearer**。
+- `GET /admin/llm/usage` **不按 `session_id`** 过滤/分组(尽管该列已落库)。
+- `POST /audio/speech` 的请求体 `voice`/`format` **当前不生效**(合成遵循服务端 `VOICE_TTS_VOICE_ID`/`VOICE_TTS_FORMAT`)。
+- `/audio/voice-clone` **真建音色**需公网参考音频 URL(当前无,待提供)+ 北京地域 API Key。
+- 可观测三帧 `observability_*`:`?level=` 分级过滤**由客户端本地做**(帧广播给所有 owner/admin 连接,订阅只决定循环跑不跑)。审计 tab 仍 5s REST 轮询(**未做** `observability_audit` 帧)。
+- **`thinking` 恒缺席(v0.66 口径更正)**:消息 DTO 的 `thinking` 在**所有**读取面都不出现(`history.Message.Thinking` 全仓无写入者)。思维链只有实时 `thinking.*` 帧(owner/admin)。**旧文档称「历史思维链可由 `GET /sessions/{id}/messages` 返回」是错的**,见 §3.4.4。
+- **消息 DTO 的 `turnId` 只在 Web/网关回合存在**:IM 渠道桥**不经过**网关编排器 ⇒ 其落库行的 `turn_id` 为空、DTO 里 `turnId` 键缺席(预期行为)。`turn_begin` / `steer_ack` / `interrupt.turnId` 同理只存在于网关回合。
+- **`GET /api/v1/media/{name}` 读侧要 Bearer owner/admin(v0.67 登记)**:音频 URL **不能**直接贴进 `