只读审计 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
151 lines
9.5 KiB
Markdown
151 lines
9.5 KiB
Markdown
# 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`)先数这段对话:
|
||
|
||
```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 拍板:**思考文本不计入对话上限**,并把两个目标拆成两条上限。
|
||
|
||
1. **对话额度只数用户读得到的东西**:助手正文 `text` + 用户提问 `text`。`thinkingText`、`thinkingSections` 不计入。额度数值 **200,000 不变**——变的只是数什么。预期从约 19 轮提到约 50 轮。
|
||
2. **另设一条物理上限护住数据库行**,把**全部字段**(含三个 receipt)都算进去。它的作用是防止单行异常膨胀,正常使用下不应该先于对话额度触发。
|
||
3. **两档都返回同一个 `session_full`**。用户看到的处置一样(开新对话),没必要让前端分两种文案,也就不用动路由和客户端。
|
||
4. **不删、不截断任何已存字段。** 「老轮次的思考文本要不要继续留着」是另一个产品问题,见 §9 观察项,本单不碰。
|
||
5. **200 条消息那档上限保留不动。**
|
||
|
||
## 4. 硬红线
|
||
|
||
1. `append_consultation_question` 必须保持 **advisory lock、`request_id` 幂等、满员拒绝** 三件事(BUG-464 防复发原文)。本单只改「数什么」和「加一档」,不得动这三条。
|
||
2. 迁移必须对**当前已部署的那一版代码向后兼容**(AGENTS §7.6):`CREATE OR REPLACE` 保持函数签名与返回列不变,`error_code` 仍只用既有的取值。staging 迁移在部署之前自动应用,旧代码必须能继续正常调用。
|
||
3. 不得新增列、不得删列、不得改类型(本轮不动表结构)。
|
||
4. 不得放宽 `char_length(v_text) > 16000` 那条单条提问长度校验。
|
||
5. 物理上限的数值必须由算术推出来并写进迁移注释,不得拍脑袋写一个整数。
|
||
6. 不得顺手升级依赖、不得顺手修不在本单里的 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. 让步顺序
|
||
|
||
1. 5.1 必须做,它是本单的全部意义。
|
||
2. 5.2 必须和 5.1 同轮——只放宽不设物理上限,等于把行大小的护栏整个拆掉。
|
||
3. 5.3 可以退化成「写成环境缺口」,但不得跳过不提。
|
||
4. 5.4 不得砍。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
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 自动迁移必须向后兼容的那一段。
|
||
|
||
验收命令:
|
||
|
||
```bash
|
||
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`)
|