只读审计 origin/staging @ 6b3248bf 后出的三份任务书,含产品 2026-09-15
拍板的三项口径:
- external-evidence-cache(BUG-727/728,Python,串行在 api-server-decomposition
之后):每轮每域同步等 api.vedastro.org,cProfile 前三名全是 TLS 往返,本地
swisseph 只占 0.022 s;前台 1.5 s / 后台 8 s / 2 个 worker 且超时不 cancel。
定案按「出生数据+岁差+交点+UTC 日期」缓存,与引擎内部同键;不许「干脆不调」,
那会重开 BUG-301。另 western_evidence_packet 122 KB 无人读。
- context-memory(BUG-729/730/731,TS,可并行):droppedCount 算了没人读、
摘要阈值与预算脱钩(窗口 < 70,667 即每轮静默丢)、写满时不继承摘要。
定案静默继承,摘要文本永不由客户端提供。
- session-capacity(BUG-732,一份迁移,可并行):200,000 额度一半被思考文本
吃掉,约 19 轮即写满。定案思考文本不计入,另设按整条 JSON 计的物理上限。
纯文档推送,不触发门禁、不发布镜像、不部署。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
175 lines
11 KiB
Markdown
175 lines
11 KiB
Markdown
# TASK · 普通聊天的三条记忆缺口:丢了不留痕、阈值追不上预算、写满不继承
|
||
|
||
- 日期:2026-09-15
|
||
- 基线 commit:`origin/staging` @ `6b3248bf`
|
||
- 执行分支:`codex/consultation-context-memory-20260915`
|
||
- 独占文件:`frontend/src/lib/consultation-session-history.ts`、`frontend/src/lib/session-context-summary.ts`、`frontend/src/app/api/consult/route.ts`、`frontend/src/app/api/sessions/route.ts`、`frontend/src/lib/chat-session-write-contract.ts`、`frontend/src/hooks/use-session-management.ts`、`frontend/src/hooks/use-consultation-run.ts`
|
||
- 与 external-evidence-cache 单、session-capacity 单无文件重叠,可并行
|
||
- 规模:一处留痕、一处算式、一次服务端复制。**不改摘要机制本身。**
|
||
|
||
---
|
||
|
||
## 1. 背景:机制是对的,坏在三个边界
|
||
|
||
BUG-555 建立的检查点摘要机制本身没问题:尾巴超过 16,000 字就在结算后异步让模型写一份 ≤800 汉字的滚动摘要存进 `chat_sessions.context_summary`,下一轮把摘要挂在最后一条用户消息里,历史只带摘要之后的部分。本单不动这套机制,只修它的三个边界。
|
||
|
||
## 2. 事故实证
|
||
|
||
### 2.1 BUG-729 · 丢掉整轮历史时不留任何痕迹
|
||
|
||
`consultation-session-history.ts` 的 `consultationHistoryWindow`:
|
||
|
||
```
|
||
let kept = clipped;
|
||
while (kept.length > 0 && kept.reduce(...) > budget) {
|
||
kept = kept.slice(1);
|
||
droppedCount += 1;
|
||
}
|
||
...
|
||
return { tail: kept, summaryText, droppedCount };
|
||
```
|
||
|
||
`droppedCount` 被算出来了,然后——`route.ts` 只取 `.tail`:
|
||
|
||
```
|
||
const historyWindow = consultationHistoryWindow(chatSession.messages, contextSummary, {...});
|
||
const storedHistory = historyWindow.tail;
|
||
```
|
||
|
||
全仓检索 `droppedCount`,**除了定义处没有任何读取点**。对照同一个文件里的 `clipConsultationHistoryText`:单条消息被截断时会追加 `omissionMarker()`——「……(以下省略 N 字,结论已并入会话摘要)」。**单条被砍有标记,整轮被丢没有。** 模型不知道自己少看了几轮,只会当成没发生过;用户问「刚才你说的那个时间」时,模型会以为自己没说过。
|
||
|
||
这是 BUG-555 防复发那句话的漏网之鱼:原文写的是「咨询历史不得再按固定 12 条 × **头部截断**静默砍结论」,整轮丢弃不属于「头部截断」,所以没被拦住。
|
||
|
||
### 2.2 BUG-730 · 「什么时候写摘要」和「能带多少历史」是两个各写各的数
|
||
|
||
| 常量 | 值 | 谁决定 |
|
||
| --- | --- | --- |
|
||
| `historyBudgetChars(contextWindow)` | `clamp((窗口 − 60,000) × 1.5, 4,000, 40,000)` 字符 | 跟着模型上下文窗口走 |
|
||
| `CONSULTATION_HISTORY_TAIL_MAX_CHARS` | **16,000** 字符(写死) | 与窗口无关 |
|
||
|
||
`shouldCheckpoint()` 用后者,`consultationHistoryWindow()` 用前者。分界线是 **70,667**:
|
||
|
||
| 上下文窗口 | 历史预算 | 写摘要阈值 | 后果 |
|
||
| ---: | ---: | ---: | --- |
|
||
| 200 k | 40,000 | 16,000 | 正常,摘要先于丢弃发生 |
|
||
| 128 k(`DEFAULT_MODEL_CONTEXT_WINDOW`,未配置时) | 40,000 | 16,000 | 正常 |
|
||
| 64 k | **6,000** | 16,000 | 预算低于阈值 → 每轮静默丢最老的几轮 |
|
||
| 32 k | **4,000**(下限) | 16,000 | 同上,且只剩一两轮历史 |
|
||
|
||
`context_window` 来自后台模型目录(`model-catalog.ts` 的 `PublishedModelRow.context_window`),是运营可配置的值。**后台上架一个中等窗口的模型就会触发**,代码里没有任何断言拦这件事。注意这条与 2.1 是叠加的:预算追不上阈值 → 每轮丢 → 丢了还不留痕。
|
||
|
||
### 2.3 BUG-731 · 写满时服务端有摘要,新对话一个字不带
|
||
|
||
`use-consultation-run.ts` 收到 409 `session_full` 后弹提示并给出「开新对话」按钮,走 `use-session-management.ts` 的:
|
||
|
||
```
|
||
async function continueInNewChat(prompt: { question: string; theme: Theme }) {
|
||
setSessionFullPrompt(null);
|
||
const created = await startNewChat();
|
||
if (!created) return;
|
||
setDraft(prompt.question);
|
||
setDraftTheme(prompt.theme);
|
||
}
|
||
```
|
||
|
||
新建一个空会话,只把用户那句问题填回输入框。`context_summary` 既没被带过去,而且 `SESSION_LIST_COLUMNS` 与 `sessions/[id]` 的 `sessionSelect` **都不含这个列**——前端想带也拿不到。用户在最需要延续的那一刻被要求从零开始。
|
||
|
||
## 3. 决策记录
|
||
|
||
产品 2026-09-15 授权本单,并明确:
|
||
|
||
1. **新对话继承摘要是「静默继承」。** 服务端把 `context_summary` 复制进新会话即可,**不加任何「接着上次聊」之类的提示**,界面看不出差别。产品原话:静默继承。
|
||
2. **摘要文本永远不许由客户端提供。** `chatSessionCreateSchema` 现在是 `.strict()` 且不含 `context_summary`,本单不得为了省事把它加进去——那等于开一个把任意文本注入模型提示词的口子。客户端只能传**来源会话的 uuid**,由服务端校验归属后自己复制。
|
||
3. **丢历史留痕用的是既有的摘要位置,不是新开一个字段。** 和 `omissionMarker` 同一套说法,不发明第二种表达。
|
||
4. **不动摘要机制本身**:不改 800 汉字上限、不改 15 秒 ref 计时器、不改乐观并发写、不改「结算后异步做」。
|
||
|
||
## 4. 硬红线
|
||
|
||
1. **摘要超时不得用 `AbortSignal.timeout()`**(BUG-555 防复发;BUG-523 的教训是它始终 unref,会让进程提前退出而不是等)。现有 `composedAbortSignal` 里那个 ref 住的 `setTimeout` 不得替换。
|
||
2. **溢出重试不得新增用户可见等待态**(BUG-555 防复发)。本单不碰那条路径。
|
||
3. **不得把 `messages` 加回列表 GET**(BUG-464 防复发)。本单要读 `context_summary`,只能按会话单取,不得顺手把列表接口的列加宽。
|
||
4. 摘要与留痕文本里不得出现出生日期、出生时间、出生地、姓名、邮箱(`sanitizeSessionContextSummary` 已在做,本单不得放宽)。
|
||
5. `frontend/src/app/page.tsx` 一行不许动(1951/2000,AGENTS §6)。
|
||
6. 本轮**不改数据库结构**(`context_summary` 列已存在),因此不得顺带动迁移(AGENTS §7.6)。
|
||
7. 不得顺手升级依赖、不得顺手修不在本单里的 warning。
|
||
|
||
## 5. 任务分解
|
||
|
||
### 5.1 丢整轮必须留痕(BUG-729)
|
||
|
||
`consultationHistoryWindow` 的 `droppedCount > 0` 时,在交给模型的摘要位置追加一句与 `omissionMarker` 同风格的说明(例如「更早的 N 轮问答已并入上面的会话摘要」)。落点在 `consultationUserTurnContent` 的 `summaryText` 段,`route.ts` 把 `droppedCount` 传进去。没有摘要却发生了丢弃时(见 5.2,理论上应被消除)也必须留痕,措辞要诚实——那种情况下结论**没有**并入摘要。
|
||
|
||
- 验收:新增断言——构造一段超预算历史,断言模型可见文本里出现丢弃说明,且句中的轮数等于 `droppedCount`。
|
||
- 验收:`droppedCount === 0` 时不得出现这句话(不许无条件加)。
|
||
- 验收:源码合同断言 `route.ts` 读取了 `droppedCount`(防止再次只取 `.tail`)。
|
||
|
||
### 5.2 写摘要的阈值跟着预算走(BUG-730)
|
||
|
||
`CONSULTATION_HISTORY_TAIL_MAX_CHARS` 从写死常量改成由 `historyBudgetChars()` 派生的函数(取预算的一个比例,比例写成具名常量并说明依据),并加一条**断言**:任何合法上下文窗口下,`阈值 < 预算`。
|
||
|
||
- 验收:新增表驱动测试,覆盖 200 k / 128 k / 64 k / 32 k / null 五种窗口,逐条断言 `checkpointThreshold(w) < historyBudgetChars(w)`。
|
||
- 验收:128 k 下的行为与改前逐字一致(阈值仍应落在 16,000 附近;若比例导致它变化,必须在进度记录里写「原值 / 新值 / 原因」三栏)。
|
||
- 验收:`shouldCheckpoint` 与 `tailCharCount` 的调用方全部改用新函数,没有残留的字面量 16,000。
|
||
|
||
### 5.3 写满时静默继承摘要(BUG-731)
|
||
|
||
三处小改,按此顺序:
|
||
|
||
1. `chatSessionCreateSchema` 增加可选字段 `continued_from_session_id: z.string().uuid().optional()`(保持 `.strict()`,**不加 `context_summary`**)。
|
||
2. `POST /api/sessions`:拿到该字段后,用当前用户身份读源会话的 `context_summary`(`.eq("user_id", user.id)`),只有读到才写进新行;读不到、不属于该用户、或为空都静默跳过,不报错。
|
||
3. `continueInNewChat` 把当前会话 id 传给 `startNewChat`,由它带进创建请求。
|
||
|
||
界面不得出现任何新提示(产品定的静默继承)。
|
||
|
||
- 验收:新增合同测试——带 `continued_from_session_id` 创建会话时,新行的 `context_summary` 等于源会话的;源会话属于**别的用户**时新行为 null 且接口仍返回 201。
|
||
- 验收:源码合同断言 `chatSessionCreateSchema` 里没有 `context_summary` 字段(拒绝客户端提供摘要文本)。
|
||
- 验收:UI 断言——`session_full` 后点「开新对话」,界面上不出现任何新增提示文案。
|
||
|
||
### 5.4 三条 Bug 历史
|
||
|
||
同一变更内写进 `docs/BUG_HISTORY.md`:
|
||
|
||
- **BUG-729**:关联 **BUG-555**,写明它的防复发只写了「头部截断」,整轮丢弃不在字面里所以没拦住。防复发升级为:**咨询历史任何形式的丢弃(截断单条、丢整轮)都必须在模型可见文本里留痕。**
|
||
- **BUG-730**:关联 BUG-555。防复发:**触发摘要的阈值必须由历史预算派生,并由一条断言钉死「阈值 < 预算」在所有合法上下文窗口下成立。**
|
||
- **BUG-731**:关联 BUG-464(`session_full` 的来源)、BUG-555。防复发:**会话满员后的「开新对话」出口必须由服务端继承 `context_summary`;摘要文本任何时候都不得由客户端提供。**
|
||
|
||
## 6. 让步顺序
|
||
|
||
1. 5.2(阈值跟预算)最先做——它是另外两条的上游,做完之后 5.1 的触发面会小很多。
|
||
2. 5.1 次之,改动最小。
|
||
3. 5.3 最后,它跨的文件最多。砍了要在进度记录里写明,并说明用户仍会在写满时丢掉记忆。
|
||
4. 5.4 不得砍。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/consultation-context-memory-20260915 \
|
||
.worktrees/consultation-context-memory-20260915 origin/staging
|
||
cd .worktrees/consultation-context-memory-20260915/frontend
|
||
git status -sb | head -1
|
||
npm ci
|
||
```
|
||
|
||
验收命令:
|
||
|
||
```bash
|
||
./node_modules/.bin/tsc --noEmit
|
||
npm run lint # 0 error
|
||
npx tsx --test tests/consultation-context.test.ts tests/consultation-*.test.ts \
|
||
tests/chat-session-*.test.ts tests/session-*.test.ts
|
||
npx tsx --test tests/*.test.ts # 与基线逐条比对失败清单(无 Docker 时数据库套件照常红)
|
||
npm run build # `/` 仍须 ○ Static,首屏 gzip ±2%
|
||
```
|
||
|
||
## 8. BUG 编号起点
|
||
|
||
基线 `6b3248bf` 上最大号 **BUG-720**;校正四单预占 **721–726**,external-evidence-cache 单预占 **727 / 728**。本单预占 **BUG-729 / 730 / 731**。开工时核对实际最大号,冲突顺延并写进进度记录。
|
||
|
||
## 9. 不在本单范围
|
||
|
||
- 摘要机制本身(800 汉字上限、15 秒计时器、乐观并发、结算后异步)
|
||
- 对话字符上限与思考文本的计入口径(见 `TASK-consultation-session-capacity-20260915.md`)
|
||
- 外网证据缓存(见 `TASK-consultation-external-evidence-cache-20260915.md`)
|
||
- 会话列表 / 详情接口的列宽(BUG-464 红线)
|