只读审计 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
11 KiB
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 授权本单,并明确:
- 新对话继承摘要是「静默继承」。 服务端把
context_summary复制进新会话即可,不加任何「接着上次聊」之类的提示,界面看不出差别。产品原话:静默继承。 - 摘要文本永远不许由客户端提供。
chatSessionCreateSchema现在是.strict()且不含context_summary,本单不得为了省事把它加进去——那等于开一个把任意文本注入模型提示词的口子。客户端只能传来源会话的 uuid,由服务端校验归属后自己复制。 - 丢历史留痕用的是既有的摘要位置,不是新开一个字段。 和
omissionMarker同一套说法,不发明第二种表达。 - 不动摘要机制本身:不改 800 汉字上限、不改 15 秒 ref 计时器、不改乐观并发写、不改「结算后异步做」。
4. 硬红线
- 摘要超时不得用
AbortSignal.timeout()(BUG-555 防复发;BUG-523 的教训是它始终 unref,会让进程提前退出而不是等)。现有composedAbortSignal里那个 ref 住的setTimeout不得替换。 - 溢出重试不得新增用户可见等待态(BUG-555 防复发)。本单不碰那条路径。
- 不得把
messages加回列表 GET(BUG-464 防复发)。本单要读context_summary,只能按会话单取,不得顺手把列表接口的列加宽。 - 摘要与留痕文本里不得出现出生日期、出生时间、出生地、姓名、邮箱(
sanitizeSessionContextSummary已在做,本单不得放宽)。 frontend/src/app/page.tsx一行不许动(1951/2000,AGENTS §6)。- 本轮不改数据库结构(
context_summary列已存在),因此不得顺带动迁移(AGENTS §7.6)。 - 不得顺手升级依赖、不得顺手修不在本单里的 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)
三处小改,按此顺序:
chatSessionCreateSchema增加可选字段continued_from_session_id: z.string().uuid().optional()(保持.strict(),不加context_summary)。POST /api/sessions:拿到该字段后,用当前用户身份读源会话的context_summary(.eq("user_id", user.id)),只有读到才写进新行;读不到、不属于该用户、或为空都静默跳过,不报错。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. 让步顺序
- 5.2(阈值跟预算)最先做——它是另外两条的上游,做完之后 5.1 的触发面会小很多。
- 5.1 次之,改动最小。
- 5.3 最后,它跨的文件最多。砍了要在进度记录里写明,并说明用户仍会在写满时丢掉记忆。
- 5.4 不得砍。
7. 开工前置命令
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
验收命令:
./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 红线)