Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
64 lines
4.9 KiB
Markdown
64 lines
4.9 KiB
Markdown
# TASK · 会话标题守卫写库恒不命中(BUG-553 修复单)(2026-09-06)
|
||
|
||
- 基线:`origin/staging` @ `e2f4b55c`(含 BUG-553 实现 `a1956deb`)
|
||
- 分支:`codex/session-title-guard-fix-20260906`
|
||
- 执行方:coding agent;验收:Claude
|
||
- 涉及文件:`frontend/src/app/api/consult/route.ts`(只改标题守卫段)、`frontend/tests/application-billing-contract.test.ts` 或新 `frontend/tests/session-title-persist-contract.test.ts`;不动迁移、不动客户端、不动 `session-title-agent.ts`
|
||
- BUG 编号起点:**BUG-557**(开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
|
||
- 串行:与 `TASK-consultation-context-and-cache` 的 `route.ts` 改动已合入,无并行冲突
|
||
|
||
## 1. 事故实证(代码验收,未上线)
|
||
|
||
`frontend/src/app/api/consult/route.ts`:
|
||
|
||
| 行 | 事实 |
|
||
| --- | --- |
|
||
| ~L282 | `chatSession` select 读到会话标题(新会话为空或「新对话」) |
|
||
| L531 | `accounting.rpc("append_consultation_question")` —— 迁移 `20260901010000_append_consultation_question.sql` L84–90 在标题为空 / 「新对话」时把标题改写成 `left(question,14) || '…'`(≤14 字时为整句) |
|
||
| L693 | `const expectedTitle = chatSession.title`(RPC **之前**的值) |
|
||
| L710 | `update({ title }).eq("id").eq("user_id").eq("title", expectedTitle)` |
|
||
|
||
对任何新会话,L710 的 `title = expectedTitle` 都比不上 L531 已改写的值,更新影响 0 行、Supabase 不报错、`console.warn` 不触发。模型标题只在客户端结算时通过 `persistSession` 的 PATCH 落库;用户中途刷新、关标签、换设备时 `api/consult/status` 回读的是问题截断标题,起名白做。`tests/session-title-agent.test.ts` 与 `application-billing-contract.test.ts` 都没有断言这条守卫会命中。
|
||
|
||
## 2. 根因
|
||
|
||
任务书 §3 决策 3「只在标题仍等于本轮开始时读到的值时更新」写的是「本轮开始」,而本轮真正改标题的是 RPC;执行方按字面取了 RPC 前的快照。守卫本身设计没错,取值时机错。
|
||
|
||
## 3. 决策记录
|
||
|
||
1. 起名触发条件(`shouldGenerateSessionTitle(expectedTitle-before-RPC, storedHistory)`)**不变**:仍用 RPC 之前的标题判断「是否自动派生」,否则 ≤14 字的问题会被当成用户手改标题而不起名。
|
||
2. 守卫的比较值改为 **RPC 之后的真实标题**:RPC 成功后再 `select("title")` 一次(一次主键查询,可接受),把该值作为 `expectedTitle` 传给守卫。不用 TS 重算 SQL 规则(`safeQuestionTitle` 与 SQL 的标点处理不同,重算会再次错配)。
|
||
3. 用户中途改名以用户为准这条语义保持:守卫仍是 `.eq("title", <RPC 后标题>)`。
|
||
4. 守卫更新要 `.select("id")` 回读影响行数,0 行时 `console.warn("session title guard missed")`,不再静默。
|
||
|
||
## 4. 硬红线
|
||
|
||
- 不改 RPC、不改迁移、不改客户端 `use-consultation-run.ts`。
|
||
- 不得把守卫放宽成无条件写入。
|
||
- 不得为测试重写路由结构;用现有 `application-billing-contract.test.ts` 的源码合同方式或 mock Supabase 链的方式二选一。
|
||
|
||
## 5. 任务分解
|
||
|
||
### 5.1 守卫取 RPC 后标题
|
||
- `route.ts`:`appendedQuestion` 成功后 `const { data: titleRow } = await supabase.from("chat_sessions").select("title").eq("id", sessionId).eq("user_id", userId).maybeSingle()`;`shouldGenerateSessionTitle` 仍用 RPC 前标题;守卫 `.eq("title", titleRow?.title ?? expectedTitle)`;更新后 `.select("id")`,长度 0 → `console.warn`。
|
||
- 验收:测试——模拟 RPC 前标题「新对话」、RPC 后标题「问题前 14 字…」、模型标题「X」:断言 update 链的 `eq("title", …)` 收到的是 RPC 后标题;模拟用户中途把标题改成「我的标题」(回读值与守卫值不等)→ 更新 0 行且 `console.warn` 被调用一次、返回的流事件仍是 `session.title`。
|
||
|
||
### 5.2 记录
|
||
- `docs/BUG_HISTORY.md` BUG-557(关联 BUG-553;说明为何 a1956deb 的测试没拦住:无守卫命中断言);`CHANGELOG.md` 一条;`docs/tasks/PROGRESS-session-title-guard-fix-20260906.md`;`docs/testing/session-list-title-and-order-20260906.md` 补一条「首轮生成中刷新页面,回来后标题仍是模型标题」。
|
||
|
||
## 6. 让步顺序
|
||
|
||
5.1 不可拆;5.2 不可省。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/session-title-guard-fix-20260906 .worktrees/session-title-guard-fix-20260906 origin/staging
|
||
cd .worktrees/session-title-guard-fix-20260906/frontend
|
||
ln -s /workspace/Jyotisha/frontend/node_modules node_modules # 注意:不要在已有 node_modules 的目录里再建,会产生 node_modules/node_modules 双份 React
|
||
npx tsx --test tests/session-title-agent.test.ts tests/application-billing-contract.test.ts 2>&1 | grep -E "^# (tests|pass|fail)|^not ok"
|
||
```
|
||
|
||
收尾同一条命令 fail=0,再 `tsc --noEmit`、`npm run lint`。
|