Files
Jyotisha/docs/tasks/TASK-rectification-open-retitles-session-20260915.md
T
Jesse_ChenandClaude Fable 5 3e58e85f8f docs(tasks): P0 — tie-break entry destroys the delivery card; session list lies about rectification
两组缺陷,分两单。

会话列表单追加 BUG-700/701:全仓只有 append_consultation_question 这个
咨询 RPC 写 chat_sessions.updated_at,校正的 turn 走自己的表从不推进,于是
校正会话的 updated_at 冻结在创建时刻——连着几小时答题也不动位置。叠加
BUG-699 的改名,列表变成「名字是今天、排序是旧日期」。而 resolveBootstrap-
SessionSelection 的 ?c= 只比对当前已加载那一页(SESSION_PAGE_SIZE = 40),
翻不到就判 missing、打「该对话不存在或已被删除」并清掉 URL。查过了没有任何
删除路径被触发,会话大概率仍在库里。

新单 BUG-702~704:applyLiveCandidateOffer 遇到任何未答的 choice/collect_spoken
就把 candidateOffer 从每一条消息上剥掉,而 requestTieBreak 必然写入这样一道题
——点卡上的按钮必然丢卡。按钮亮不亮看 tieBreakPersonalityAvailable(只看
followup),题能不能画成选择题看 buildChoiceCard 的 styleOptions.ok /
canRenderYearlessChoice,两个判据不一致,于是按钮亮着、点开是裸题,卡又没了,
两条路都断。交付旁白还写了 VOICE 明令禁止的「相对支持度」,并在自称不再问
分盘题的同一屏提供分盘题入口。

产品重申 BUG-686 的原意:卡上不要参考题按钮,出卡前收集完,以卡结算。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-15 07:44:40 +00:00

318 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · P0:校正会话在列表里「名字是今天、位置是旧日期、刷新后报已删除」(BUG-699/700701
- 日期:2026-09-15
- 基线 commit`origin/staging` @ `ff5023a2`= staging 当前部署)
- 执行分支:`codex/rectification-open-retitles-session-20260915`
- 工作树:`.worktrees/rectification-open-retitles-session-20260915`
- 严重度:**P0**。用户已经因此找不到自己的会话,且标题损坏是**写进服务端、不可自动恢复**的。
---
## 1. 用户现象(原话)
> 我现在根本找不到那条 session 了。这个历史对话列表的排序和命名都有问题。之前的比如 12 号的我刚点开,里面就成 15 号的了。然后一大堆 15 号的记录,但实际上都不是 15 号的。
---
## 2. 事故实证
行号会漂移,定位以符号名为准。核对于 `origin/staging@ff5023a2`
### 2.1 根因(单点,已定位)
`frontend/src/hooks/use-rectification-surface.ts``openRectificationCase`,构造 `merged` 的那一段:
```ts
const existing = sessions.find((session) => session.id === opened.sessionId);
const merged: ChatSession = {
id: opened.sessionId,
title: resolveSessionTitle("生时校正", undefined, { // ← 无条件重算
entrypoint: "birth_time_rectification",
existingTitles: sessions.map((session) => session.title),
}),
updatedAt: timestamp(), // ← 无条件盖成此刻
pinned: existing?.pinned ?? false, // ← 这两个保留了
archivedAt: existing?.archivedAt ?? null, // ←
};
setSessions((current) => [merged, ...current.filter((s) => s.id !== merged.id)]);
void persistSession(merged).catch(() => {}); // ← 写回服务端
```
`existing` 已经查出来了,`pinned``archivedAt` 也确实从它继承——**`title``updatedAt` 却没有**。作者知道这条路径会跑在已存在的会话上,只是漏了两个字段。
`resolveSessionTitle` 的日期来自墙钟:
`frontend/src/lib/agent-reply.ts`
```ts
const at = options.at ?? new Date();
return uniquifySessionTitle(datedSessionTitle(at, "生时校正"), existingTitles, at);
// datedSessionTitle: `${at.getMonth() + 1}月${at.getDate()}日 · ${suffix}`
```
所以打开一条 9 月 12 日建的校正会话,标题被算成「9 月 15 日 · 生时校正」。若当天已经有同名的,`uniquifySessionTitle` 再追加 `时:分`,于是列表里出现一堆「9月15日 · 生时校正」「9月15日 · 生时校正 14:32」……
### 2.2 两种损坏的持久性不同(修复时必须分开对待)
`frontend/src/hooks/use-session-management.ts``persistSession``update` 分支写的字段是:
```
title, theme, model_id, chart_profile_id, chart_profile_name, chart_profile_role
```
**不含 `updated_at`**(这是 BUG-553 的修复结果,正确,不要动)。因此:
| 损坏 | 范围 | 刷新后 |
| --- | --- | --- |
| **标题被改成今天** | 写进服务端 `chat_sessions.title` | **不恢复。永久损坏。** |
| **排到列表最前 / 落进「今天」分组** | 只改客户端 `sessions` state 的 `updatedAt` | **自行恢复**(服务端 `updated_at` 未被写坏) |
这条区分是本单最重要的事实:**排序是暂时的,命名是永久的。** 已经被改坏的历史标题需要一次数据修补,见任务 3。
### 2.3 入口覆盖面
`openRectificationCase` 有三个调用方,**三个都会触发**:
- `openRectificationSession(exactSessionId)` —— **点侧栏历史校正会话**,就是用户踩到的那条
- `openRectificationFromHomepage()` —— 首页校正卡
- 第三个 `"new"` 入口 —— 新建
只有 `"new"` 该重算标题与 `updatedAt`;另外两个作用在已存在的会话上。
### 2.4 普通咨询会话不受影响(不要顺手改)
`use-consultation-run.ts` 的三处 `resolveSessionTitle` 都有守卫:
- 两处是 `session.messages.length === 0 && isGenericSessionTitle(session.title)` —— 只对全新空会话生效。
- 一处只在模型返回了非通用标题时调用,走的是 `clipTitle(modelTitle)` 分支,根本到不了带日期的分支。
**本单只改校正这一条路径。**
### 2.5 这是 BUG-553 的复发
`docs/BUG_HISTORY.md`**BUG-553 | 历史列表只按置顶排,改名收藏后旧会话跳到最顶**resolved`a1956deb`)。它的防复发写的是:
> 元数据 PATCH 不得写 `updated_at`。**`updatedAt` 只由对话活动推进。** 侧栏历史排序必须置顶 + `updatedAt` 倒序。
「打开一个历史会话」不是对话活动。BUG-553 修的是元数据 PATCH 与客户端改名/收藏/归档/换模型/换资料五条路径,**没有覆盖校正的 open 路径**,那条路径当时也已经存在。防复发是对的,但没有任何测试或类型约束去执行它,所以另一条路径照样违反。
BUG-553 也是「命名+排序」一起坏,用户这次的描述与当时几乎一字不差。
---
### 2.6 追加实证(2026-09-15 第二次回报):另外两条独立缺陷
用户随后回报:「刷新之后也恢复不到这个 session,返回『该对话不存在或已删除』,左边 session 列表也没有。」URL 是 `/?c=6cf7653b-c33d-4f1d-b4c4-bb190eed03ff`,会话标题是「9月15日 · 生时校正 15:19」(`uniquifySessionTitle` 追加了时分,说明当天已有同名标题——即 §2.1 的改名已经发生过不止一次)。
查证后**没有找到任何删除路径**`grep -rn 'method: "DELETE"' frontend/src` 里只有一处作用于会话,是 `use-session-management.ts` 的显式删除动作;服务端 `DELETE /api/sessions/[id]` 也只被它调用。会话大概率仍在库里。真正让它「消失」的是下面两条。
#### BUG-700|校正活动从不推进 `chat_sessions.updated_at`,会话边用边下沉
全仓只有一处写 `chat_sessions.updated_at`
`frontend/supabase/migrations/20260901010000_append_consultation_question.sql`
```sql
updated_at = clock_timestamp()
```
这是**普通咨询**的 RPC。校正的 turn 走 `/api/rectification/cases/[caseId]/turns` 与自己的表,**不碰 `chat_sessions.updated_at`**`grep -rn "chat_sessions" src/lib/rectification-agentic/v9/` 无命中)。
于是一条校正会话的 `updated_at` **冻结在创建时刻**。用户连着几小时在里面答题,它在侧栏的位置纹丝不动。
和 §2.1 的改名叠加,结果是列表**自相矛盾**:
- **显示的名字**:今天(被 open 改的)
- **实际的排序与分组**:创建那天(`updated_at` 从未推进)
「一大堆 15 号的记录但实际上都不是 15 号的」正是这个——名字全变成今天,位置还在各自的旧日期。
BUG-553 的防复发「`updatedAt` 只由对话活动推进」在这里被反向违反:**校正答题是不折不扣的对话活动,却不推进。**
#### BUG-701`?c=` 找不到就报「已被删除」,从不问服务端
`frontend/src/lib/chat-session-url.ts``resolveBootstrapSessionSelection`
```ts
if (query.sessionId && input.listedIds.includes(query.sessionId)) { }
return { sessionId: input.defaultSessionId, urlAction: "replace-clear", missing: true, };
```
`listedIds` 只是**当前已加载那一页**的 id。`missing: true` 会让 `page.tsx` 打出 `SESSION_MISSING_NOTICE``"该对话不存在或已被删除"`),并且把 URL 里的 `?c=` 清掉。
而列表是游标分页的:`frontend/src/lib/session-cursor.ts``SESSION_PAGE_SIZE = 40``GET /api/sessions``updated_at desc, id desc` 取 40 条(置顶另算)。被 BUG-700 压在旧日期上的校正会话,只要前面攒了 40 条更新的,首屏就加载不到它。
**「不在已加载的这一页」被当成了「已被删除」**,还顺手把用户唯一能回去的 URL 抹了。这是本次「找不到 session」最直接的原因,也是三条里最容易修的。
## 3. 决策记录(产品已授权)
1. **打开已有会话不得改标题、不得改 `updatedAt`。** 只有新建才算首次命名。
2. 已经被改坏的历史标题**要修回**,见任务 3。修不回真实日期的,宁可退回中性标题,也不要留一个错的日期。
3. 本单只改校正 open 路径与随之而来的守卫,不重构会话列表。
4. **校正答题必须推进 `updated_at`**(BUG-700)。它和普通咨询一样是对话活动,BUG-553 的规则本来就该覆盖它。
5. **`?c=` 指向的会话不在已加载页时,必须先问服务端再下结论**(BUG-701)。确认服务端也没有,才允许说「已被删除」;在此之前不得清掉 URL 里的 `?c=`
---
## 4. 硬红线
1. **不得修改 `sortSessions` / `groupSessionsByRecency` / `recencyKeyFor`**`frontend/src/lib/session-groups.ts`)。排序口径是对的,坏的是喂给它的数据。
2. **不得让 `persistSession` 开始写 `updated_at`**——那正是 BUG-553 修掉的东西。
3. **不得改 `use-consultation-run.ts` 的三处标题守卫**(§2.4,它们是对的)。
4. `tsc --noEmit` 0 错;`npm run lint` **0 error**
5. 测试总数不得低于基线;改既有断言写「原值 / 新值 / 原因」三栏。
6. 全量 `npm test` 的失败清单必须与基线 `ff5023a2` 逐条一致(Linux 上基线是 27 条环境失败)。
7. 任务 3 的数据修补**不得猜测日期**,不得凭 `created_at` 以外的来源编造。
---
## 5. 任务分解
### 任务 1 · BUG-699:打开已有会话不再改名、不再 bump
**1.1** `frontend/src/hooks/use-rectification-surface.ts``openRectificationCase``title``updatedAt` 改为**优先继承 `existing`**,与 `pinned` / `archivedAt` 同一写法:
```ts
title: existing?.title ?? resolveSessionTitle("生时校正", undefined, { }),
updatedAt: existing?.updatedAt ?? timestamp(),
```
**1.2** `existing` 不存在时(真正的新建)才走 `resolveSessionTitle``timestamp()`,行为不变。
**1.3** 顺带确认 `messages: []` 这一行:`merged` 无条件把本地消息清空,随后才 `hydrateRectificationCase`。校正面自己从 turns 渲染,所以界面上可能看不出问题,但 `persistSession``update` 分支不写 `messages`,服务端不受影响。**确认后在 PROGRESS 里写结论**;若确实无害就保持原样,不要顺手改。
**验收标准**
- 打开一条已有的历史校正会话:侧栏标题不变、分组不变、位置不变。
- 新建校正:标题仍是「{今天} · 生时校正」,出现在「今天」最上面。
- 从首页校正卡打开一个**已存在**的可续校正:同样不改名、不移位。
### 任务 2 · 把防复发从「文字约定」变成「测试」
BUG-553 的防复发只写在 Bug 记录里,没有任何东西执行它,所以又被另一条路径违反。本轮补上:
**2.1** 新增 `frontend/tests/session-open-preserves-identity.test.ts`(名字可调),至少断言:
- `openRectificationCase``existing` 命中时产出的会话,`title``updatedAt` **全等于** `existing` 的值。
- `existing` 未命中时才调用 `resolveSessionTitle`
**2.2** 新增一条**跨文件的源码契约**,锁住 BUG-553 的那句防复发:`frontend/src/hooks/``frontend/src/lib/` 里,任何构造 `ChatSession` 且可能作用于已有会话的地方,不得无条件写 `updatedAt: timestamp()`
具体形式执行方定(可以是"`timestamp()` 出现在 ChatSession 字面量里时,同一字面量必须出现 `existing?.updatedAt ??` 或注释豁免标记"这类文本断言)。**要求是:下一个人再写一条这样的路径时会被测试拦下**,而不是又只留一句 Bug 记录里的话。若实在写不出稳定的文本契约,退到 2.1 并在 PROGRESS 说明缺口。
**2.3** `resolveSessionTitle``options.at` 默认 `new Date()` 是墙钟。加一行注释说明:**它只能用于新建**;任何已有会话的重算都必须显式传入该会话的创建时间,否则日期会变成今天。
**验收标准**:新测试通过;把 1.1 的继承改回无条件重算,新测试必须变红(执行方本地验一次,结论写进 PROGRESS,不要把破坏提交上去)。
### 任务 3 · 修回已经被改坏的历史标题
**3.1 先量损坏面。** 写一个**只读**的核对脚本或 SQL(不改数据),统计 `chat_sessions``session_type = 'birth_time_rectification'``title` 匹配 `^\d{1,2}月\d{1,2}日\s*·\s*生时校正(\s+\d{2}:\d{2})?$`、但**标题里的月日与 `created_at` 的本地月日不一致**的行数。数字写进 PROGRESS。
**3.2 修补规则**,按可靠性排序,**不得猜**
1. `created_at` 可用 → 标题重写为 `created_at` 本地日期的「M月D日 · 生时校正」。这是唯一真实来源。
2. `created_at` 不可用 → 退回中性标题「生时校正」,**不带日期**。宁可没有日期,也不要错的日期。
**3.3 形式**:写成一次性迁移还是运维脚本,由执行方按 `deploy/README.md` 的既有做法定,但:
- 必须**幂等**。
- 必须**只动匹配上的行**,不得批量覆盖。
- 必须先在 staging 跑,把影响行数贴进 PROGRESS,再由产品决定要不要在生产跑(生产当前停在 `7b620c7a`,是否受影响需单独确认,见 §8)。
- 用户自己手动改过的标题(不匹配 3.1 的正则)**一律不动**。
**验收标准**staging 上跑完,3.1 的核对脚本返回 0 行;随机抽 3 条核对标题日期等于 `created_at` 本地日期。
### 任务 5 · BUG-700:校正活动推进 `updated_at`
**5.1** 校正写入 turn(以及采用、停止这类推进会话状态的动作)时,同步把该会话的 `chat_sessions.updated_at` 推到当前时间。
实现位置由执行方按既有架构定——优先复用 `append_consultation_question` 那种**服务端 RPC 内部顺带更新**的做法,**不要**让客户端 `persistSession` 开始写 `updated_at`(§4 红线 2)。
**5.2** 只有真正的对话活动才推进:写 turn、提交选择、采用、停止。**打开、刷新、读取快照、改名、收藏、归档一律不推进**(BUG-553 的规则,继续有效)。
**5.3** 已经冻结的历史数据:校正会话的 `updated_at` 若明显早于其最后一条 turn 的时间,按**最后一条 turn 的时间**回填。与任务 3 同一次数据修补里做,同样要求幂等、只动匹配行、先在 staging 报影响行数。
**验收标准**
- 在一条旧校正会话里答一道题,侧栏里它移动到「今天」最上面。
- 打开同一条会话但不答题,位置不变。
- 回填后,抽查 3 条校正会话的 `updated_at` 等于其最后一条 turn 的时间。
### 任务 6 · BUG-701`?c=` 找不到时先问服务端
**6.1** `resolveBootstrapSessionSelection``listedIds` 未命中时,**不得直接判定 missing**。改为返回一个「需要向服务端确认」的状态,由 `page.tsx``GET /api/sessions/{id}` 单条查询:
- 服务端有 → 把这条会话并进本地列表并选中它,URL 保持 `?c=` 不变。
- 服务端 404 → 这时才是真的没有,打 `SESSION_MISSING_NOTICE`、清 `?c=`
- 请求失败(网络 / 5xx)→ **既不选中也不宣告删除**,保留 `?c=` 并给可重试的提示。不得把一次网络抖动说成「已被删除」。
**6.2** 文案复核:`SESSION_MISSING_NOTICE` 现在是「该对话不存在或已被删除」。只有在服务端确实 404 时才允许出现这句。
**6.3** 顺带确认侧栏:会话确实存在但在后面的分页里时,侧栏的无限滚动(`app-sidebar.tsx``session-list-sentinel` + `loadMoreSessions`)能把它翻出来。若选中了一条不在已加载页的会话,侧栏应当能正确高亮它——查一下这条是否成立,结论写进 PROGRESS;不成立就单独记一条,**不要在本单顺手改**。
**验收标准**
- 构造一条排在第 40 条之后的会话,用它的 `?c=` 直接打开:能正常进入,URL 不被清,不出现「已被删除」。
- 服务端确实 404 的 id:仍然给出「该对话不存在或已被删除」并清 `?c=`
- 单条查询失败时:不宣告删除,`?c=` 保留。
- 新增测试覆盖上面三条分支。
### 任务 4 · 测试与文档
**4.1** 基线:
```
cd .worktrees/rectification-open-retitles-session-20260915/frontend
npm ci
npm test 2>&1 | tail -20
```
**4.2** 文档:
- `docs/BUG_HISTORY.md` 新增 **BUG-699 / BUG-700 / BUG-701**,字段齐全。BUG-700 的「复发自」同样写 `BUG-553`——它的规则说 `updatedAt` 只由对话活动推进,而校正答题就是对话活动却没推进,是同一条规则的另一半没落实。BUG-701 写清「不在已加载页」被当成「已删除」的判定错误。**「复发自」写 `BUG-553`**,并在根因里写明 BUG-553 的防复发为何没拦住——它只是一句话,没有任何测试执行它,且当时只覆盖了元数据 PATCH 与五条客户端路径,漏了校正 open。回到 BUG-553 记录的「防复发」末尾补一句指向 BUG-699 与新测试。
- `frontend/DESIGN.md`:在会话列表/Copy 一节补一句——**会话标题在创建时定名,打开已有会话不得改名;`updatedAt` 只由对话活动推进**,并指向新测试。
- `CHANGELOG.md`:一条日期 + 一句话标题 + 要点(含"历史校正标题已修回")。
- `docs/testing/rectification-open-identity-20260915.md`:真机/浏览器清单——打开 3 条不同日期的历史校正,确认标题、分组、位置都不变;新建一条确认仍按今天命名。
- `docs/tasks/PROGRESS-rectification-open-retitles-session-20260915.md`
---
## 6. 让步顺序
1. 任务 2.2 的跨文件契约若写不出稳定形式,**退到 2.1 的单元测试**并把缺口写进 PROGRESS。1.1 的修复不让。
2. 任务 3 若 `created_at` 在相当比例的行上不可用,**先只修 `created_at` 可用的部分**,其余列成清单交回产品决定,不要擅自退回中性标题以外的任何猜测。
3. 任务 1.3`messages: []`)若查出确有问题,**不要在本单顺手修**,另开一条,本单只记录。
4. **绝不让步**:不得改 `sortSessions` / 分组口径;不得让 `persistSession``updated_at`;不得改咨询侧三处标题守卫;数据修补必须幂等且不猜日期。
---
## 7. 开工前置命令
```bash
cd /workspace/Jyotisha
git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/rectification-open-retitles-session-20260915 \
.worktrees/rectification-open-retitles-session-20260915 origin/staging
cd .worktrees/rectification-open-retitles-session-20260915/frontend
npm ci
npm test 2>&1 | tail -20
./node_modules/.bin/tsc --noEmit
npm run lint
```
交付:`git push origin HEAD:staging`,推完核对远端 SHA。
---
## 8. 需要产品确认 / 本单未覆盖
| 项 | 说明 |
| --- | --- |
| 生产是否受影响 | 生产停在 `7b620c7a`2026-08-16)。该提交里 `use-rectification-surface` 是否已有这段 `merged` 构造,需执行方 `git show 7b620c7a:frontend/src/hooks/use-rectification-surface.ts` 核对后写进 PROGRESS。若是,任务 3 的修补要不要在生产跑由产品定。 |
| 用户丢失的那条会话 | 服务端 `updated_at` 没坏,刷新后排序会回到真实位置;只是标题写着今天的日期。可以按**位置**(在「最近 7 天」里 12 号那一段)找回。 |
| BUG 编号 | BUG-698 由 `TASK-settings-dialog-size-and-nav-20260915.md` 占用。本单占 **BUG-699 / 700 / 701**`TASK-rectification-tiebreak-card-loss-20260915.md`**BUG-702704**。 |