Files
Jyotisha/docs/tasks/TASK-consultation-context-memory-20260915.md
T
Jesse_ChenandClaude Opus 5 11893c7f97 docs(tasks): 普通聊天审计三单(外网缓存 / 记忆三缺口 / 对话上限)
只读审计 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
2026-09-15 22:47:55 +00:00

175 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/2000AGENTS §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**;校正四单预占 **721726**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 红线)