e2d6f20361
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
4.9 KiB
4.9 KiB
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) |
| 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. 决策记录
- 起名触发条件(
shouldGenerateSessionTitle(expectedTitle-before-RPC, storedHistory))不变:仍用 RPC 之前的标题判断「是否自动派生」,否则 ≤14 字的问题会被当成用户手改标题而不起名。 - 守卫的比较值改为 RPC 之后的真实标题:RPC 成功后再
select("title")一次(一次主键查询,可接受),把该值作为expectedTitle传给守卫。不用 TS 重算 SQL 规则(safeQuestionTitle与 SQL 的标点处理不同,重算会再次错配)。 - 用户中途改名以用户为准这条语义保持:守卫仍是
.eq("title", <RPC 后标题>)。 - 守卫更新要
.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.mdBUG-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. 开工前置命令
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。