feat(memory): 记忆整理「试算(dry-run)」入口 + 面板卡(契约 v0.75)
主人要「现在就想看到在 736 条上它会建议合并哪些」,而不是等 23:00 的低峰首跑。
- 后端:Tidier.DryRun + TidyService.DryRun,**复用同一条扫描→召回→判定→构造候选路径**(抽出 tidyRoundPolicy,生产轮与试算轮只差「候选出口」与「召回降级策略」两处;禁止第二套实现)
- 🔴 零副作用:不写 memory_tidy_candidates / memory_tidy_runs,绝不改 memory_items 任何列;机械证明 = 跑前跑后 cyrene.db 与 -wal 的 SHA256 一致(测试断言「表行数不变」)
- 失败码诚实:500 tidy_unavailable / 409 tidy_disabled / 500 tidy_recall_unavailable(**embedding 不可用不降级成 0 条**)/ 503 tidy_timeout / 500 tidy_dry_run_failed;**空数组唯一含义 = 算过了没建议**
- 端点 POST /api/v1/admin/memories/tidy/dry-run(writeAuth);面板「记忆」页加试算卡(点了才跑、无轮询、三态分开、明写「仅试算,不会改动任何记忆」)
- 真机 e2e(741 条活库副本):76 ms / 15 分区 / 53 候选 / 41 skipped / 1 vetoed(low_confidence);样例 merge reason=fact_subset score=1.0000、link reason=similar_but_not_contained score=0.9248
- 变异:dry-run 顺手写库 ⇒ 两条测试红(行数 0→2);自造阈值 ⇒ 红;失败回 200 空数组 ⇒ 红
- 如实登记:Compared=688 被 400/分区护栏截断、候选上限 20/分区被打满 ⇒ 护栏值需在 741 条量级重定(非 bug)
This commit is contained in:
1 parent
3d8c30dc99
commit
d6ed9cfa2b
18 files changed
+1631
-79
No files matched your search
+87
-11
@@ -1,6 +1,6 @@
|
||||
# Cyrene v2 客户端 API 契约
|
||||
|
||||
> **契约版本:`v0.74`**(上一版 `v0.73`,其条目见 §6;`v0.72` / `v0.71` 及更早见本文件 §6,`v0.67` 及更早的基线已归档、只读:`Docs/archive/api-client.v0.65.md`)。
|
||||
> **契约版本:`v0.75`**(上一版 `v0.74`,其条目见 §6;`v0.73` / `v0.72` / `v0.71` 及更早见本文件 §6,`v0.67` 及更早的基线已归档、只读:`Docs/archive/api-client.v0.65.md`)。
|
||||
> **读者**:Web 前端 / 移动端原生客户端(含 Android)/ IM 桥接等**跨平台客户端开发者**。
|
||||
> **唯一真源 = 当前代码**(`internal/services/gateway/*` 等)。本文档由代码逐条核对得出;与代码不符时**以代码为准**,并回改本文档。
|
||||
> **旧的超详细版已归档、只读**:`Docs/archive/api-client.v0.65.md`(412 KB,含 v0.28→v0.65 全部历史条目)与 `Docs/archive/api-reference.v0.65.md`(170 KB,后端全量字段级文档)。归档的原因是**结构性臃肿 + 历史条目并列 + 长期漂移**;本文档**不复制**那段历史,需要考古时去归档。
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
> **v0.74 是 IM 路由主动消息的提醒版(已实现,纯增量)**:`message_persisted` 的 `data` 新增**可选**结构化判据 `proactive:true` —— IM 路由(`im_bridge`)的主动推送在落库之后**追加**一条带该键的同类帧,客户端据此在**非前台**补一发浏览器通知(此前通知只挂在 `proactive_message` 上,而 `auto` 提示在主人有已连接 IM 渠道时**优先走 IM** ⇒ 实际最可能发生的路径恰恰不弹)。判据是**结构化字段**(不是正文/提示词):普通消息(Web 回合 / IM 回复 / IM 镜像)的帧**永不**带该键 ⇒ 不可能被误判成主动消息。同一行仍照旧发一条**不带**该键的普通镜像帧(v0.72 起的行为逐字不变,两帧同 id ⇒ 客户端按 id 去重后仍是一个气泡、仍只提醒一次)。详见 **§3.4.4 后的「`message_persisted` 的 `proactive` 判据」**。
|
||||
|
||||
> **v0.75 是记忆整理试算版(已实现,纯增量)**:新增 1 条管理端点 **`POST /api/v1/admin/memories/tidy/dry-run`**(**只读语义的 POST**,`writeAuth` = owner/admin)—— 跑**同一套**记忆整理规则(召回门 + 包含判定 + 规则否决,与低峰定时轮共用代码路径),把候选**直接回在响应里**,**一行都不写**(不写 `memory_tidy_candidates` / `memory_tidy_runs`,绝不改 `memory_items`)。修的是「整理器只在 23:00–07:00 跑(design/23 §4),在它第一次开跑前面板只能空着」——主人要「现在就想看到在 736 条上它会建议合并哪些」。**零副作用是硬约束**(后端有逐行快照单测钉死);失败**一律如实作答**(`tidy_unavailable` / `tidy_disabled` / `tidy_recall_unavailable` / `tidy_timeout`),**绝不回空候选数组假装「没有建议」**。**无破坏性变更**(新端点;既有端点/帧一字未改)。详见 **§2.2 B14 / §4.12**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 基础约定
|
||||
@@ -129,12 +131,12 @@
|
||||
| 档位 | 中间件 | 条数 | 说明 |
|
||||
|---|:--:|:--:|---|
|
||||
| **只读监控档** | `viewerAuth`(owner/admin/viewer) | **12** | 非敏感投影:无凭据、无他人账号数据、无消息正文 |
|
||||
| **owner/admin 读档** | `writeAuth`(owner/admin) | **13** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71) |
|
||||
| **owner/admin 读档** | `writeAuth`(owner/admin) | **14** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75) |
|
||||
| **owner-only 档** | `ownerAuth`(owner) | **40** | 带审计的策略写、每个获取凭据的渠道端点、其余全部变更 |
|
||||
|
||||
**管理路由总数 = 12 + 13 + 40 = 65 条。**
|
||||
**管理路由总数 = 12 + 14 + 40 = 66 条。**
|
||||
|
||||
> **计数口径(v0.71 明确)**:总数是 §2.1 / §2.2 / §2.3 三张清单里**列出的行数**(与 v0.68 起的口径一致)。⚠️ `internal/services/gateway/routes.go` 里实际经 `writeAuth` 挂载的 **admin** 路由比 §2.2 多:v0.67 登记的 `GET /admin/channels/events` 与 `.../counters`(见 §2.4(a))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上面的 13。逐条以代码为准。
|
||||
> **计数口径(v0.71 明确)**:总数是 §2.1 / §2.2 / §2.3 三张清单里**列出的行数**(与 v0.68 起的口径一致)。⚠️ `internal/services/gateway/routes.go` 里实际经 `writeAuth` 挂载的 **admin** 路由比 §2.2 多:v0.67 登记的 `GET /admin/channels/events` 与 `.../counters`(见 §2.4(a))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上面的 14。逐条以代码为准。
|
||||
|
||||
> ⚠️ **与归档版不一致(已更正)**:`Docs/archive/api-client.v0.65.md` §2.0 写「12 + 11 + 38 = 61」(其 v0.62 条目又写「12 + 10 + 38 = 60」,自相矛盾)。**以代码为准 = 12 + 11 + 40 = 63**。
|
||||
|
||||
@@ -155,7 +157,7 @@
|
||||
| A11 | GET | `/api/v1/admin/observability/logs` | 后端日志尾部(最新优先,`?level=&limit=`;缓冲未注入时 501) |
|
||||
| A12 | GET | `/api/v1/admin/notifications` | 统一通知 feed(审计链 + 后台思考推送的只读投影) |
|
||||
|
||||
### 2.2 owner/admin 读(13 条,`writeAuth`)
|
||||
### 2.2 owner/admin 读(14 条,`writeAuth`)
|
||||
|
||||
| # | 方法 | 路径 | 要点 |
|
||||
|---|---|---|---|
|
||||
@@ -172,6 +174,7 @@
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
> **读取 `GET /admin/observability/{logs,metrics,trace}` 兼任可观测推送的订阅握手**(该凭据的连接全部断开即退订)。REST 响应形状不变。
|
||||
|
||||
@@ -774,6 +777,63 @@
|
||||
|
||||
---
|
||||
|
||||
### 4.12 记忆整理「试算」(`POST /api/v1/admin/memories/tidy/dry-run`;v0.75 新增)
|
||||
|
||||
**为什么有这条**:记忆整理是一件**低峰定期后台任务**(`design/23 §4`):默认只在安静时段(`MEMORY_TIDY_QUIET_START_HOUR`–`_END_HOUR`,默认 `23:00–07:00`)由心跳开跑。于是在它第一次开跑之前,库里**既没有候选也没有轮次留痕** ⇒ 面板只能空着,主人无法判断「整理会不会乱合并」。本端点把**同一套规则**当场算一遍:候选直接回在响应里,**一行都不写**。
|
||||
|
||||
#### (a) 请求与响应
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 方法 / 路径 | `POST /api/v1/admin/memories/tidy/dry-run` |
|
||||
| 鉴权档 | `writeAuth`(**owner / admin**;`viewer` → `403`,未认证 → `401`)。与同页的 `/admin/config-facts`、`/admin/memories` 同档 |
|
||||
| 请求体 | **可为空**(端点不读 body)。没有参数:阈值/上限一律取**进程生效的既有配置**(`MEMORY_TIDY_RECALL_THRESHOLD` / `_SCAN_LIMIT` / `_CANDIDATE_LIMIT`),端点**自己不带任何旋钮** |
|
||||
| 响应体 | `{data:{dryRun,recallMode,durationMs,ruleVersion,partitions,scanned,compared,pairs,vetoed,vetoReasons,truncated,candidates[]}}` |
|
||||
| `dryRun` | 恒为 `true`。它是**结构化**判据:这份响应**没有**改动任何记忆(客户端不必只靠本地硬编码文案说明这件事) |
|
||||
| `recallMode` | `"embedding"`(语义向量召回)或 `"lexical"`(零依赖词项余弦,仅当进程没有接 embedder) |
|
||||
| `durationMs` | 本次试算的墙钟耗时(毫秒) |
|
||||
| `ruleVersion` | 规则集版本(`internal/runtime/memory` 的 `TidyRuleVersion`,进候选幂等键;**改判定规则必须同时升它**) |
|
||||
| 观测口径 | `partitions`(含在册条目的分区数)/ `scanned`(扫描条数)/ `compared`(**真正参与两两比对**的条数,受性能护栏上限约束)/ `pairs`(相似度 ≥ 召回门的对数)/ `vetoed` 与 `vetoReasons`(否决数与原因分布,如 `scope`/`persona`/`project`/`source`/`temporal`/`numeric`/`conflict`/`low_confidence`) |
|
||||
| `truncated` | **可选**数组:被护栏截断的分区标签(`<分区>(scan)` / `(compare)` / `(candidates)`)。它**不是**候选;键缺席 = 本轮没有任何截断。命中它说明「配置的配额比本轮实际看到的少」 |
|
||||
| `candidates[]` | **恒为非 null 数组**(没有建议时是 `[]`)。每个元素见下表 |
|
||||
| 归档 | **不审计**:它是只读计算(跑规则、不落库),与 `/admin/config-facts`、`/admin/flow/graph` 同一档 |
|
||||
|
||||
候选元素(`candidates[i]`):
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `action` | `merge` / `link` / `archive` / `reclassify`(`keep` 不进候选队列)。**`link` = 相似但互不包含 ⇒ 只建链接、绝不合并**(design/23 §1.3 的「红牛 vs 冰美式」) |
|
||||
| `sourceIds` | 被处理的记忆 id 数组(恒为非 null)。`merge` 时是被并入方(可多条);`link` 时是那两条 |
|
||||
| `targetId` | `merge` 时的**存活条目**(超集);其余动作为 `""`(键省略) |
|
||||
| `personaId` / `scope` / `projectId` / `kind` | 分区键与被处理条目的 kind |
|
||||
| `proposedContent` / `mergedContent` | `merge` 的 diff 预览:被并入的原文 / 存活条目的内容(其余动作为 `""`) |
|
||||
| `reason` | 机器可读理由(如 `containment_a_subset_b` / `similar_but_not_contained` / `one_off_event` / `kind_mismatch`)。**客户端按它分支,不要按本地文案** |
|
||||
| `ruleVersion` / `scoreMin` / `scoreMax` | 规则版本与召回分数区间(当前实现两者相等:一对一个分数) |
|
||||
| `evidence` | 判定证据:`similarity`(只用它**召回**,不是判据)、`recall_mode`、`containment`(`a_subset_b`/`b_subset_a`/`both`/`none`)、`containment_evidence[]`(缺哪一要素)、`veto_reason`、`kind_a`/`kind_b`/`proposed_kind`、`content_a`/`content_b`(两条原文,供面板 diff 预览) |
|
||||
|
||||
#### (b) 硬约束(客户端必须照这个读)
|
||||
|
||||
1. 🔴 **零副作用**:本端点**不写任何表** —— 不写 `memory_tidy_candidates`、不写 `memory_tidy_runs`、**绝不改 `memory_items` 的任何列**。后端有「调用前后三张表逐行快照比对」的单测把它钉死。因此试算**不会**让面板的「候选队列」多出东西,也**不影响**当晚那次真跑的结果。
|
||||
2. 🔴 **同一套规则,不是第二套实现**:候选由与低峰定时轮**共用**的代码路径产出(同一召回门 + 同一包含判定 + 同一规则否决 + 同一每分区候选上限)。后端另有一条单测:同一批数据上「试算候选」与「真跑一轮落库的候选」逐字段一致。
|
||||
3. 🔴 **失败绝不伪装成「没有建议」**:未接线 / 配置关闭 / 语义召回不可用 / 超时都**如实回错误码**(见下表),**不回** `candidates:[]`。客户端**不得**把 `candidates:[]` 与错误态渲染成同一件事 —— 前者是「算过了,这一轮确实没有建议」,后者是「没算出来」。
|
||||
4. **语义召回不可用时不静默降级**:若进程接了 embedder 而它不可用(如 `llama.cpp / :11434` 连不上),试算**报 `tidy_recall_unavailable`** 而不是偷偷回退到词项余弦 —— 否则面板会把「语义路径没跑起来」当成「没有候选」。⚠️ 这是**试算专属**的严格:低峰定时轮与手动轮仍按既有口径回退到零依赖的词项余弦(`recallMode:"lexical"` 时可从 `evidence.recall_mode` 逐条读到)。
|
||||
5. **只算不写 ⇒ 不审计,也不产生候选队列**:本端点不写审计链、不产生任何后续动作;apply/reject 是**另外的**、尚未实现的写端点(`design/23 §11`,本版**不做**)。
|
||||
6. **它是「现在就能看」,不是「调度改了」**:试算**不受安静时段约束**(低峰窗口是心跳的调度口径,不是规则的一部分),也**不改变**整理周期与心跳(试算不写 `lastRun`、不影响 `nextDelay`)。面板**按下才跑**:它是一次真实规则计算(扫描 + 两两召回 + 包含判定),**不是看板指标** ⇒ 客户端**不要**自动轮询它。
|
||||
|
||||
#### (c) 失败码
|
||||
|
||||
| 状态 | `error.code` | 触发条件 | 客户端应做什么 |
|
||||
|---|---|---|---|
|
||||
| `500` | `tidy_unavailable` | 组合根未注入试算出口(仅异常部署/测试) | 显示「未接线」,**不要**显示「没有建议」 |
|
||||
| `409` | `tidy_disabled` | `MEMORY_TIDY_ENABLED=false`(显式关闭整理) | 提示「整理已被配置关闭」,并指向该 env 键 |
|
||||
| `500` | `tidy_recall_unavailable` | 语义召回(embedding 服务)不可用 | 显示具体原因类别;**不要**显示 0 候选 |
|
||||
| `503` | `tidy_timeout` | 试算超过服务端墙钟上限(3 分钟) | 可提示重试;**不要**显示 0 候选 |
|
||||
| `500` | `tidy_dry_run_failed` | 其它内部失败(分区查询/规则执行出错) | 显示错误;原始内部细节**不会**出现在 `message` 里 |
|
||||
|
||||
> ⚠️ 本端点的错误 `message` 只说明**原因类别**,原始错误(可能含内网地址等内部细节)只进服务端日志。
|
||||
|
||||
---
|
||||
|
||||
## 5. env 与模型配置(与契约相关)
|
||||
|
||||
> **`.env` vs 面板文件**:面板「LLM 模型」页保存写 `<DATA_DIR>/models.json`(默认 `./data/models.json`);「渠道管理」页写 `<DATA_DIR>/channels.json`。**只要文件存在且有效,运行时优先用它**,`.env` 只作兜底/播种层。`.env.example` 与实现同步(键名逐字一致、值占位符)。
|
||||
@@ -808,11 +868,11 @@
|
||||
| `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 读到生效值) |
|
||||
| `MEMORY_TIDY_ENABLED` | `true` | 记忆整理总开关(`design/23 §4`)。关闭 ⇒ 完全不跑(日志写明);整理**只产建议、绝不自动改记忆** |
|
||||
| `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` |
|
||||
| `MEMORY_TIDY_RECALL_THRESHOLD` | `0.80` | 候选**召回门**(余弦),**不是**合并判据(可否合并由包含关系 + 规则否决决定) |
|
||||
| `MEMORY_TIDY_SCAN_LIMIT` / `MEMORY_TIDY_CANDIDATE_LIMIT` | `5000` / `20` | 每分区每轮的扫描上限 / 候选上限 |
|
||||
| `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 读取面。
|
||||
| `SUBAGENT_TIMEOUT_SEC` | `600` | 单个子代理整次运行时限 |
|
||||
@@ -837,7 +897,20 @@
|
||||
> 只保留最近若干版(**v0.60 起**);更早的全部历史见归档 `Docs/archive/api-client.v0.65.md` §5(v0.28 → v0.59)。
|
||||
> 每版一句话 + 三档标注。**三档定义见 [§0.4](#04-三档标注惯例)。**
|
||||
|
||||
### v0.74(当前契约版本)
|
||||
### 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 键语义**一字未改**。
|
||||
- **零副作用是硬约束**:端点走的是与低峰定时轮**共用**的规则路径(同一召回门 + 同一包含判定 + 同一规则否决 + 同一每分区候选上限),但**候选不落库** —— 不写 `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` 的镜像口径)。
|
||||
|
||||
@@ -1054,6 +1127,8 @@
|
||||
| **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.75 **只**加了「试算」B14):试算只「算给我看」、**不改任何记忆**。`GET /admin/memories/candidates`、`POST .../candidates/{id}/{apply,reject,revert}`、`GET /admin/memories/tidy/runs`、`POST /admin/memories/tidy/run` **均尚未实现**。客户端**不要**假定它们存在(`design/23` 的第一版口径本就是「只产建议,apply 才写记忆」,apply 未做的前置原因见该节开放点) |
|
||||
| **`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) |
|
||||
|
||||
### 7.3 诚实边界(既有事实)
|
||||
|
||||
@@ -1102,7 +1177,8 @@
|
||||
20. **(v0.73 登记)落盘只覆盖「保留期内」且只覆盖白名单会话**:`turns[]` 是**已落盘且未被 GC 回收**的轮次摘要,**不是全历史**(`FLOW_GRAPH_RETENTION` 默认 168h,`≤0` = 关闭 GC ⇒ 那时才会无限增长,属显式选择)。落盘范围与实时发布**同一白名单**(`flowSessionAllowed`:`web` / `web:<uuid>` / `sess-backthink`)—— **群聊 / 陌生人会话永远不会出现在 `turns[]` 里**。此外**正在跑、尚未 `EndRound`** 的那一轮不会在 `turns[]` 中出现(它是内存里的「实时」那一轮,`source:"live"`),这是如实而非缺失。
|
||||
|
||||
21. **(v0.74 登记)`message_persisted` 的 `proactive` 判据只活在实时帧上,不落库**:它是「这条是她主动说的话」在线上**唯一**的结构化判据(帧顶布尔键,由 `gateway.BroadcastProactivePersisted` 打上),**没有对应数据库列**(本片不动 schema)。因此 ① `GET /sessions/{id}/messages`、握手 `history` 回放、`GET /admin/channels/**` 读到的历史行**不带**它(预期:通知只对实时到达有意义);② 客户端**不得**从 `platform` / `adapterId` / `turnId` 等行字段反推「是不是主动消息」—— 主会话里主动推送写的是**解析后的真实平台**(例如 `weixin-ilink`),与普通回复行同一形状,反推必然误伤。**落库失败时不发该帧**(没有权威 `id` 的 `message_persisted` 会被客户端整帧丢弃)。
|
||||
22. **(v0.75 登记)试算端点(B14)是「只读计算」,它**不改**任何状态**:它不写 `memory_tidy_candidates` / `memory_tidy_runs`、不改 `memory_items`、不写审计链、**也不推进心跳**(不写 `lastRun`、不影响 `nextDelay`)。因此 ① 试算**不会**让「候选队列」(尚未实现,见 §7.2)多出任何东西;② 试算结果**不是缓存**:每次调用都重新算一遍,两次调用之间数据变了结果就可能不同(这是「同一套规则」的正常语义,不是不确定性),客户端**不要**把它当成会稳定的列表去 diff 或自动轮询;③ **`recallMode:"lexical"` 是降级信号**:它表示这轮没有走到语义向量(进程没接 embedder),此时召回更保守(命中更少)—— 若进程**接了** embedder 而它不可用,试算**直接报错**(`tidy_recall_unavailable`),**不会**在响应里假装成 lexical。
|
||||
|
||||
---
|
||||
|
||||
*本文件由「以代码为真源」维护(当前 `v0.74`;`v0.73` / `v0.72` / `v0.71` / `v0.70` / `v0.69` / `v0.68` 见本文件 §6,`v0.67` 及更早见本文件 §6 与 `Docs/archive/`)。范围:`v0.74` 同步了 `internal/services/gateway/**`(`proactive.go` / `broadcast.go`)、`internal/services/channelbridge/**`(`proactive.go` / `persist.go` / `proactive_notify.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/lib/ws/handlers/**` 与本文件;`v0.73` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`internal/runtime/flowgraph/**`、`internal/db/migrations/0022_flow_graphs.*`、`frontend/web/src/**` 与本文件;`v0.72` 同步了 `internal/services/gateway/**`(`proactive*`)、`internal/services/backthink/**`、`frontend/web/src/**` 与本文件;`v0.71` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`frontend/web/src/**` 与本文件。*
|
||||
*本文件由「以代码为真源」维护(当前 `v0.75`;`v0.74` / `v0.73` / `v0.72` / `v0.71` / `v0.70` / `v0.69` / `v0.68` 见本文件 §6,`v0.67` 及更早见本文件 §6 与 `Docs/archive/`)。范围:`v0.75` 同步了 `internal/services/gateway/**`(`admin_memory_tidy.go` / `routes.go` / `deps.go` / `server.go`)、`internal/runtime/memory/**`(`tidy_dryrun.go` / `tidy_recall.go` / `tidy.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/**` 与本文件;`v0.74` 同步了 `internal/services/gateway/**`(`proactive.go` / `broadcast.go`)、`internal/services/channelbridge/**`(`proactive.go` / `persist.go` / `proactive_notify.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/lib/ws/handlers/**` 与本文件;`v0.73` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`internal/runtime/flowgraph/**`、`internal/db/migrations/0022_flow_graphs.*`、`frontend/web/src/**` 与本文件;`v0.72` 同步了 `internal/services/gateway/**`(`proactive*`)、`internal/services/backthink/**`、`frontend/web/src/**` 与本文件;`v0.71` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`frontend/web/src/**` 与本文件。*
|
||||
Reference in new issue
Block a user