只读审计 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
9.5 KiB
TASK · 对话上限一半被思考文本吃掉,十几轮就「已写满」
- 日期:2026-09-15
- 基线 commit:
origin/staging@6b3248bf - 执行分支:
codex/consultation-session-capacity-20260915 - 主要落点:新增一份迁移(
CREATE OR REPLACE FUNCTION public.append_consultation_question)、frontend/tests/下的合同测试 - 不改
frontend/src/app/api/consult/route.ts:两档上限都继续返回同一个session_full,路由与客户端一行都不用动 - 与 external-evidence-cache 单、context-memory 单无文件重叠,可并行
1. 事故实证
问下一个问题之前,append_consultation_question(supabase/migrations/20260901010000_append_consultation_question.sql)先数这段对话:
select coalesce(sum(
length(coalesce(elem->>'text', ''))
+ length(coalesce(elem->>'thinkingText', ''))
+ case when elem ? 'thinkingSections'
then length((elem->'thinkingSections')::text) else 0 end
), 0) into v_chars from jsonb_array_elements(...) as elem;
if v_count >= 200 or (v_chars + v_new_chars) > 200000 then
return query select false, 'session_full'::text;
超了就 409,文案「这段对话已写满,开个新对话继续吧」。
每一轮一问一答实际吃掉多少:
| 计入的内容 | 每轮字符 | 用户读得到吗 |
|---|---|---|
助手正文 text |
约 3,000 – 5,000 | 是,这才是对话 |
思考文本 thinkingText |
最多 4,000(route.ts 里 slice(0, 4_000)) |
默认折叠 |
思考分节 thinkingSections |
1,521 / 2,243 / 2,977(实测 1 / 2 / 3 个问题域) | 只是步骤标签 |
| 用户提问 | 约 20 – 200 | 是 |
分节那三个数是跑 natalConsultationThinkingPlan 实测的 JSON 长度(3 / 4 / 5 个 section,19 / 29 / 39 个 step)。合计每轮约 9,000 – 12,000 字符,一半以上是模型内部状态。
200,000 ÷ 每轮约 10,500 ≈ 19 轮。另一档上限 200 条消息(100 轮)永远碰不到——真正卡住用户的是字符数,而字符数一半是思考。
口径本来就不一致:同一条助手消息里还存了 techniqueTruth、workflowReceipt、agentExecutionReceipt,这三个字段照样入库却完全不计入上限。
2. 根因
这条上限身兼二职却一职没做好:
- 想当「这段对话有多长」→ 不该数用户读不到、也无法控制的思考文本。
- 想当「护住数据库那一行」→ 那就该把
techniqueTruth/workflowReceipt/agentExecutionReceipt一起数,可它没数。
两个目标挤在一个数字里,结果是两边都不成立:用户的对话额度被模型内部状态吃掉一半,而数据库行的真实大小从来没被这条上限约束过。
3. 决策记录
产品 2026-09-15 拍板:思考文本不计入对话上限,并把两个目标拆成两条上限。
- 对话额度只数用户读得到的东西:助手正文
text+ 用户提问text。thinkingText、thinkingSections不计入。额度数值 200,000 不变——变的只是数什么。预期从约 19 轮提到约 50 轮。 - 另设一条物理上限护住数据库行,把全部字段(含三个 receipt)都算进去。它的作用是防止单行异常膨胀,正常使用下不应该先于对话额度触发。
- 两档都返回同一个
session_full。用户看到的处置一样(开新对话),没必要让前端分两种文案,也就不用动路由和客户端。 - 不删、不截断任何已存字段。 「老轮次的思考文本要不要继续留着」是另一个产品问题,见 §9 观察项,本单不碰。
- 200 条消息那档上限保留不动。
4. 硬红线
append_consultation_question必须保持 advisory lock、request_id幂等、满员拒绝 三件事(BUG-464 防复发原文)。本单只改「数什么」和「加一档」,不得动这三条。- 迁移必须对当前已部署的那一版代码向后兼容(AGENTS §7.6):
CREATE OR REPLACE保持函数签名与返回列不变,error_code仍只用既有的取值。staging 迁移在部署之前自动应用,旧代码必须能继续正常调用。 - 不得新增列、不得删列、不得改类型(本轮不动表结构)。
- 不得放宽
char_length(v_text) > 16000那条单条提问长度校验。 - 物理上限的数值必须由算术推出来并写进迁移注释,不得拍脑袋写一个整数。
- 不得顺手升级依赖、不得顺手修不在本单里的 warning。
5. 任务分解
5.1 对话额度只数正文
新迁移里 CREATE OR REPLACE 该函数,把 v_chars 的求和改成只累加 elem->>'text',thinkingText 与 thinkingSections 不再进这个和。上限常数 200,000 不变。
- 验收:
npm run test:db --prefix frontend(需 Docker)新增用例——同一段会话,助手消息带 4,000 字thinkingText与 3,000 字thinkingSections时,不再影响还能问几轮;只有正文长度影响。 - 验收:无 Docker 时写成环境缺口进
BLOCKED.md,并用 SQL 静态合同断言(求和表达式里不含thinkingText/thinkingSections)替代,不得写成「通过」。 - 验收:幂等分支(同
request_id重入返回true, null)、advisory lock、session_missing三条既有行为的测试一条不改仍全绿。
5.2 加一条把全部字段算上的物理上限
同一个函数里加第二次求和:对每条消息取 length(elem::text)(整条 JSON 的长度,字段增减都自动覆盖),超过物理上限同样返回 session_full。
数值按算术定,并把算式写进迁移注释:50 轮 × 每轮(正文约 4,000 + 思考 4,000 + 分节 3,000 + 三个 receipt 约 3,000)≈ 700,000,取 1,000,000 作为留有余量的物理上限。执行方若测得 receipt 实际更大,按实测调整并在进度记录里写明新算式。
- 验收:新增用例——正文很短但 receipt 极大的会话会被物理上限拒绝,且
error_code仍是session_full。 - 验收:正常形态的会话在触发对话额度之前不会先撞上物理上限(用 5.1 的夹具跑到额度边界,断言物理上限未触发)。
- 验收:迁移注释里有那行算式。
5.3 量一次真实的详情接口体积
对话轮数上限从约 19 提到约 50,GET /api/sessions/[id] 返回的整份 messages 也会按比例变大(该接口的 sessionSelect 含 messages,打开一段长会话是整份取回)。本单不改这个接口,但必须把数字量出来:
-
构造一段跑到新额度边界的会话(测试夹具即可,不需要真实用户数据),记录详情接口 JSON 的体积。
-
结论写进进度记录。如果在 2 vCPU 上明显偏大,追加到 §9 观察项并写进
BLOCKED.md,不得默默略过。 -
验收:进度记录里有改前(约 19 轮)与改后(约 50 轮)两组体积数字。
5.4 Bug 历史
同一变更内写进 docs/BUG_HISTORY.md,预占 BUG-732。必须写明:关联 BUG-464(200 / 200,000 两档上限与 append_consultation_question 都是那一单立的),以及这不是回归,是那条上限从一开始就身兼二职。防复发写成:
会话上限必须分成两条各司其职的口径:面向用户的对话额度只数用户读得到的正文;面向存储的物理上限必须把整条消息 JSON 算全。新增会存进
messages的字段时,必须明确它进哪一条,不得默认落进对话额度。
6. 让步顺序
- 5.1 必须做,它是本单的全部意义。
- 5.2 必须和 5.1 同轮——只放宽不设物理上限,等于把行大小的护栏整个拆掉。
- 5.3 可以退化成「写成环境缺口」,但不得跳过不提。
- 5.4 不得砍。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/consultation-session-capacity-20260915 \
.worktrees/consultation-session-capacity-20260915 origin/staging
cd .worktrees/consultation-session-capacity-20260915
git status -sb | head -1
cd frontend && npm ci
开工前必读:docs/BUG_HISTORY.md 里的 BUG-464(这两档上限的来历与三条防复发),以及 AGENTS §7.6 关于 staging 自动迁移必须向后兼容的那一段。
验收命令:
cd frontend
./node_modules/.bin/tsc --noEmit
npm run lint
npm run db:migrate:check # 迁移可应用性
npm run test:db # 需 Docker;无 Docker 写 BLOCKED.md
npx tsx --test tests/consultation-*.test.ts tests/chat-session-*.test.ts
npx tsx --test tests/*.test.ts # 与基线逐条比对失败清单
8. BUG 编号起点
基线 6b3248bf 上最大号 BUG-720;校正四单预占 721–726,external-evidence-cache 单预占 727 / 728,context-memory 单预占 729 / 730 / 731。本单预占 BUG-732。开工时核对实际最大号,冲突顺延并写进进度记录。
9. 观察项与不在本单范围
- 老轮次的思考文本要不要一直留着:
thinkingText会在历史消息里渲染(chat-message-row.tsx对已结算消息也判断message.thinkingText?.trim()),所以它是个真功能,不是纯浪费。但它的价值随时间衰减,只保留最近 N 轮是个可选方向——这是产品决策,本单不做,也不得顺手做。 - 详情接口分页 / 按需加载历史(5.3 只负责量数据)
- 200 条消息那档上限
- 上下文记忆三条(见
TASK-consultation-context-memory-20260915.md) - 外网证据缓存(见
TASK-consultation-external-evidence-cache-20260915.md)