docs(tasks): 会话列表重建任务书(BUG-987~989)
校正会话被 messages<>'[]' 整类过滤掉、副标题与排序不同源、 新建复用旧空草稿。产品拍板副标题改最后活动时间、第一问前不落库。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
co-authored by
Claude Opus 5
parent
0c3c9d6b42
commit
f8d65e484d
@@ -152,6 +152,7 @@
|
||||
| `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` |
|
||||
| `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 |
|
||||
| — | `PROGRESS-starter-greeting-20260917.md` | 首页开场语改成 claude.ai 式单行问候:`starter-greeting.ts` 的「称呼 + 追问句」五时段十五条收成三个池子(时段 / 星期 / 回访,按 `variantSelection` 确定性取一条),副标题行与 `.starter-salutation` 下线,h1 降到 `clamp(24px, 2.8vw, 30px)`;追问移到输入框占位符「想聊什么都可以」。产品直接拍板,非 Bug,不占 BUG 号 | 待验收 | `codex/starter-greeting-20260917` |
|
||||
| `TASK-session-list-rebuild-20260921.md` | — | **P0 会话列表重建(`e4e73f56` 的返工)**:服务端 `GET /api/sessions` 用 `messages <> '[]'` 排除空咨询,但校正会话的 `messages` 永远是 `[]`(建行写死空数组、创建合同只收空数组、PATCH 忽略、`touch_chat_session_from_rectification_case` 只 bump `updated_at`)→ **全部生时校正会话从侧栏消失**,而客户端 `isListedSidebarSession` 明确放行校正会话,两层规则相反且无跨层对断(BUG-987);同一提交把副标题换成 `created_at`,排序/分组/游标仍是 `updated_at`,列表可见时间不单调(9/18→9/17→9/7→9/16)(BUG-988);BUG-928 让步下来的「复用空会话」不 bump 时间,点新建变成跳回 9/17 的旧草稿(BUG-989)。**产品 09-21 拍板**:D1 副标题改最后活动时间(推翻 BUG-929 该条,其余保留)、D2 收掉 `BLOCKED.md:120` 的让步,第一问前不落库、删复用与 `draft`。T1 不得让步,T3 可拆轮。BUG 段 987 起 | 待领取 | — |
|
||||
|
||||
### 个人报告
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# TASK 会话列表重建:校正会话消失、显示时间与排序不同源、空会话落库
|
||||
|
||||
- 日期:2026-09-21
|
||||
- 基线 commit:`0c3c9d6b`(`origin/staging`,与 `https://staging.jyotisha.chat/api/health` 的 `deployment.gitCommit` 一致)
|
||||
- 分支:`codex/session-list-rebuild-20260921`(单分支串行,三个任务都动 `frontend/src/app/api/sessions/route.ts`,不得并行开两个 worktree)
|
||||
- BUG 编号起点:**BUG-987**(开工时以 `docs/BUG_HISTORY.md` 实际最大号为准,当前最大 `BUG-986`)
|
||||
- 关联记录:BUG-553、BUG-699、BUG-704、BUG-705、BUG-926、BUG-927、**BUG-928**、**BUG-929**、BUG-933
|
||||
|
||||
---
|
||||
|
||||
## 1. 事故实证
|
||||
|
||||
产品负责人 2026-09-21 15:56 在 `staging.jyotisha.chat` 的侧栏截图,列表只剩 6 条:
|
||||
|
||||
| 位置 | 标题 | 副标题显示 | 所在分组 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 今日节奏 · 9月18日 | 9月18日 08:01 | 最近 7 天 |
|
||||
| 2 | 新对话(当前选中) | 9月17日 22:53 | 最近 7 天 |
|
||||
| 3 | 9月16日 · 今日节奏 | **9月7日 13:42** | 最近 7 天 |
|
||||
| 4 | 我下半年的运势怎么样 职业… | 9月16日 16:01 | 最近 7 天 |
|
||||
| 5 | 深入看今日 | 9月9日 16:53 | 最近 30 天 |
|
||||
| 6 | 深入看今日 | 9月9日 16:37 | 最近 30 天 |
|
||||
|
||||
三条现象:
|
||||
|
||||
1. **一条生时校正会话都没有。** 产品负责人 09-08 至 09-17 在 staging 跑过数十轮真实校正,全部不在列表里。
|
||||
2. 显示的时间不单调(9/18 → 9/17 → 9/7 → 9/16),且 9/7 的行落在「最近 7 天」分组里(今天 9/21,7 天边界是 9/15)。
|
||||
3. 点「新建对话」没有新建,落到了 9/17 那条旧「新对话」上。
|
||||
|
||||
三条都由同一个提交 `e4e73f56`(2026-09-17,`fix(web): 四个页面共用一份会话列表,空会话不入列`)引入。
|
||||
|
||||
### 1.1 实证 A:列表过滤把整类校正会话删掉(P0)
|
||||
|
||||
`frontend/src/app/api/sessions/route.ts`,`excludeEmptyConsultations()`:
|
||||
|
||||
```
|
||||
// Empty consultations are `messages = []`. Rectification rows keep an opening
|
||||
// turn, so excluding empty arrays leaves them in the list (BUG-928).
|
||||
return query.not("messages", "eq", []);
|
||||
```
|
||||
|
||||
注释第二句与代码事实相反。校正会话的 `chat_sessions.messages` **永远是 `[]`**,三条独立证据:
|
||||
|
||||
- `frontend/src/hooks/use-rectification-surface.ts`,`openRectificationSession()` 里构造 `merged: ChatSession` 时写死 `messages: []`(`sessionType: "birth_time_rectification"` 同一对象字面量内,符号定位:`const merged: ChatSession = {`)。
|
||||
- `frontend/src/lib/chat-session-write-contract.ts`,`chatSessionCreateSchema` 的 `messages: z.array(chatMessageSchema).max(0)`——创建路径只接受空数组;`chatSessionWriteSchema` 上方注释写明 PATCH「parses this shape so old bundles are accepted, then the messages field is ignored rather than stored」。
|
||||
- `frontend/supabase/migrations/20260915010000_rectification_touch_chat_session.sql` 的 `touch_chat_session_from_rectification_case()` 只 `set updated_at = new.last_activity_at`,不碰 `messages`。校正的轮次存在 case 表里。
|
||||
|
||||
而客户端 `frontend/src/lib/session-list-filter.ts` 的 `isListedSidebarSession()` 明确放行:
|
||||
|
||||
```
|
||||
if (session.sessionType === "birth_time_rectification") return true;
|
||||
```
|
||||
|
||||
**两层规则相反,服务端先把行拿走了,客户端的放行永远轮不到执行。**
|
||||
|
||||
门禁没拦住的原因:`frontend/tests/chat-session-authority.test.ts` 只做源码正则断言
|
||||
`assert.match(listRoute, /\.not\("messages", "eq", \[\]\)/)`,从未拿真实 Postgres 行验证过「哪些行还在」。
|
||||
|
||||
深链 `?c=<uuid>` 仍能打开校正会话(`resolveBootstrapSessionSelection()` 回落 `urlAction: "lookup"` 走单条读取),但侧栏是唯一的发现入口,对用户等于历史全部丢失。
|
||||
|
||||
### 1.2 实证 B:显示的时间不是排序用的时间
|
||||
|
||||
- 显示:`frontend/src/lib/session-sidebar-row.ts` 的 `sessionSidebarSubtitle()` → `const created = session.createdAt || session.updatedAt;`
|
||||
- 排序:`frontend/src/lib/session-groups.ts` 的 `sortSessions()` → `right.session.updatedAt - left.session.updatedAt`
|
||||
- 分组:同文件 `recencyKeyFor(updatedAt, now)`
|
||||
- 服务端排序:`route.ts` 的 `.order("updated_at", { ascending: false }).order("id", { ascending: false })`,游标 `sessionCursorFilter()` 同样按 `updated_at`
|
||||
|
||||
`e4e73f56` 之前 `SESSION_LIST_COLUMNS` 里没有 `created_at`,`createdAt` 回落到 `updatedAt`,两者恰好一致;该提交把 `created_at` 加进列之后就分叉了。
|
||||
|
||||
**排序链路本身没有缺陷**:第 3 行是 9/7 创建、9/16 晚上仍在使用的会话,`updated_at` 落在第 2 与第 4 之间,位置和分组都正确。用户看到的那一列数字不是排序键,所以列表读起来是乱的。
|
||||
|
||||
### 1.3 实证 C:第一问之前就落库,新建变成复用旧草稿
|
||||
|
||||
- `frontend/src/hooks/use-session-management.ts` 的 `startNewChat()`:无 `continuedFromSessionId` 时先 `findReusableEmptyConsultation(sessions)`,命中就 `setActiveSessionId(reusable.id)` 直接返回,**既不 bump `updated_at` 也不改 `created_at`**。
|
||||
- `route.ts` 的 GET 在首页(无游标)时额外查一条 `session_type = 'consultation' and messages = []` 作为 `draft` 返回;`frontend/src/app/(app)/page.tsx` 的启动流程把它并进 `nextSessions`,所以这条空会话照常出现在侧栏,带着 9/17 的时间。
|
||||
- `BLOCKED.md` 第 120 行「会话列表:空咨询延迟落库未做(2026-09-17,BUG-928)」记录了当初的让步:任务书原本要求「第一问前才 `POST /api/sessions`」,因牵动 `?c=` 深链与刷新恢复而降级成「服务端过滤 + 复用空会话」。
|
||||
|
||||
---
|
||||
|
||||
## 2. 根因
|
||||
|
||||
一句话:**`e4e73f56` 用「`messages` 是否为空」当作「这条会话有没有内容」的判据,但这个判据只对咨询会话成立;同一提交又把副标题换成另一个时间字段,让列表的可见顺序与真实排序键脱钩。**
|
||||
|
||||
拆开三层:
|
||||
|
||||
1. **判据错位。** 「空会话」的真实定义是「用户还没开口」。咨询会话恰好可以用 `messages = []` 表达,校正会话不能——它的内容在 case 表里。服务端把一个只对一半数据成立的判据写成了全表过滤。
|
||||
2. **两套过滤规则。** 服务端 `excludeEmptyConsultations()` 与客户端 `isListedSidebarSession()` 对同一个问题给出相反答案,且没有任何测试跨这两层对断。
|
||||
3. **让步没收口。** BUG-928 让步掉「延迟落库」之后,留下的替代方案(复用空会话)没有处理复用时的时间语义,于是「新建」这个动作在用户眼里变成了「跳到一条旧记录」。
|
||||
|
||||
---
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
产品负责人 2026-09-21 就地拍板,以下两条**推翻既有记录**,执行方按本节执行,不得以旧记录为由拒改:
|
||||
|
||||
- **D1(推翻 BUG-929 的「副标题为上海时区创建时间」)**:侧栏副标题改为**最后活动时间**(`updatedAt`),与排序、分组完全同源。理由:BUG-929 当初引入创建时间是为了替代被删掉的 `uniquify` HH:MM 后缀(同日多条靠钟点区分),这个目的用最后活动时间同样达到;而与排序键分叉造成的「列表读起来是乱的」是更大的代价。BUG-929 的其余决定(标题格式「生时校正 · M月D日」「今日节奏 · M月D日」、不再追加 HH:MM、旧标题不批量改)**全部保留**。
|
||||
- **D2(收掉 `BLOCKED.md:120` 的让步)**:「第一问之前不落库」本轮必须做完。点「新建对话」只在本地开一条,用户真正发出第一句话时才 `POST /api/sessions`。复用已有空会话的逻辑随之**删除**(`findReusableEmptyConsultation` 及其调用点);服务端 GET 的 `draft` 字段随之**删除**。
|
||||
|
||||
产品负责人明确不接受的替代方案(不要再提):副标题同时显示两个日期;把排序改成按创建时间;保留复用但只 bump 时间戳。
|
||||
|
||||
---
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. **不得用「某列为空」推断会话有没有内容。** 列表要不要显示一条会话,判据只能是「用户是否已经开口」,且该判据必须对两种 `session_type` 都成立。校正会话在任何情况下都不得被列表过滤掉——这是 BUG-987 的防复发。
|
||||
2. **显示、排序、分组、游标四者必须用同一个时间字段**(`updated_at`)。T2 完成后 `SESSION_LIST_COLUMNS` 不得再含 `created_at`,让分叉在结构上不可能复发。
|
||||
3. 打开已有会话仍不得改 `title` / `updated_at`(BUG-699、BUG-553 的防复发,不得因本轮改动被推翻)。
|
||||
4. `frontend/src/app/(app)/page.tsx` 不得增长(AGENTS.md §6);新逻辑进 hook 或 `lib/`。当前行数开工时记录,交付时不得高于该值。
|
||||
5. 不得再手写第二套会话列表数据源;`(app)/layout.tsx` 的 provider 是唯一来源(BUG-927 的防复发)。
|
||||
6. 源码合同测试搬家时两端都要改(BUG-933 的教训):任何断言 `page.tsx` 或 `route.ts` 内容的测试,改了实现就必须同步断言。
|
||||
7. 不得顺手升级依赖、不得顺手修不在本任务书里的 warning。
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
### T1|列表不得吞掉生时校正会话(BUG-987,P0,先做)
|
||||
|
||||
- 把 `excludeEmptyConsultations()` 的过滤条件从「`messages <> '[]'`」改成「**咨询会话且 `messages = '[]'`** 时才排除」,即校正会话一律保留。实现上是把条件收窄到 `session_type = 'consultation'` 这一支,而不是全表按 `messages` 过滤。
|
||||
- 归档视图(`archived=1`)走同一条规则,不得出现「正常视图看不到、归档视图能看到」或反之。
|
||||
- 删掉 `route.ts` 里那句与事实相反的注释,换成说明校正会话的内容不在 `messages` 列里。
|
||||
- **验收标准:**
|
||||
- 新增真实 Postgres 测试 `frontend/tests/database-session-list-visibility.test.ts`:插入 ①`messages = []` 的咨询、②`messages = []` 的 `birth_time_rectification`、③有内容的咨询、④归档的校正,断言列表查询返回 ②③、不返回 ①,归档视图返回 ④。**不得用源码正则代替**(这正是 BUG-987 漏网的原因)。无 Docker 时按 §7 让步顺序处理。
|
||||
- `frontend/tests/chat-session-authority.test.ts` 第 16 行那条正则断言同步更新为新写法,并补一条「过滤条件必须带 `session_type` 限定」的断言。
|
||||
- 新增跨层对断:服务端过滤规则与 `isListedSidebarSession()` 对「空咨询 / 校正会话 / 归档」三种输入给出相同答案。
|
||||
- 产品负责人在 staging 侧栏能看到 09-08 以来的校正会话。
|
||||
|
||||
### T2|显示时间与排序同源(BUG-988)
|
||||
|
||||
- `sessionSidebarSubtitle()` 改用 `session.updatedAt`(D1)。
|
||||
- `SESSION_LIST_COLUMNS` 去掉 `created_at`(硬红线 2)。去掉前先确认列表路径没有第二个 `createdAt` 消费者——已核实只有副标题在读;`frontend/src/lib/rectification-session-title-repair.ts` 走的是迁移路径,不经过这个列表。
|
||||
- `frontend/src/lib/home-cloud-sync.ts` 的 `createdAt` 回落链保持不变(详情接口仍返回 `created_at`),不要顺手删。
|
||||
- **验收标准:**
|
||||
- `frontend/tests/session-groups.test.ts` 或新测试断言:对任意一组会话,副标题渲染出的时间序列与 `sortSessions()` 的输出顺序单调一致(构造一条「早创建、晚活动」的会话,断言它排在正确位置且副标题显示的是晚活动时间)。
|
||||
- 源码合同断言 `SESSION_LIST_COLUMNS` 不含 `created_at`。
|
||||
- 截图第 3 行那种情况(9/7 创建、9/16 活动)在 UI 上显示 9月16日,且位置不变。
|
||||
|
||||
### T3|第一问之前不落库(BUG-989,最后做,牵动面最大)
|
||||
|
||||
- `startNewChat()`:删除 `findReusableEmptyConsultation` 分支,改为**只在本地创建**会话对象并选中,不发 `POST /api/sessions`。
|
||||
- 首次真正发送消息时(`send()` 路径)才落库:先 `POST /api/sessions` 建行,再走既有的 `append_consultation_question`。落库失败时保持现有的回滚语义(`setSessions` 撤销 + `requestError`),不得把用户刚打的字弄丢。
|
||||
- `route.ts` 的 GET 删除 `draft` 查询与 `draft` 字段;`(app)/page.tsx` 删除 `readDraftConsultation` 的并入;`session-list-context.tsx` 删除 `draftRow`。
|
||||
- `?c=<uuid>` 深链与刷新恢复:本地未落库的会话没有服务端身份,**不得**把它的 id 写进 URL(现在 `startNewChat` 里 `writeSessionUrl(nextSession.id, "push")` 会写)。落库成功后再写 URL。刷新时未落库的本地会话消失是可接受的(它本来就是空的),但不得因此报「该对话不存在或已被删除」。
|
||||
- 清理历史遗留:本轮**不做**存量空会话的批量删除(数据清理另议),但要确认 T1 之后它们仍被列表过滤掉。
|
||||
- 更新 `BLOCKED.md:120` 那条——按 AGENTS.md §4,解除后划掉而不是删除。
|
||||
- **验收标准:**
|
||||
- `frontend/tests/session-list-lifecycle.test.tsx`(或新测试)断言:点「新建对话」后没有发生 `POST /api/sessions`;发出第一句话后恰好发生一次;落库失败时本地会话被撤销且草稿文本仍在。
|
||||
- 断言未落库的本地会话不写 `?c=`,落库成功后才写。
|
||||
- 断言 `route.ts` 响应体不再含 `draft`、`session-list-context.tsx` 不再有 `draftRow`。
|
||||
- 产品负责人在 staging 连点五次「新建对话」,侧栏不出现任何「新对话」行;发一句话后出现一行,副标题是今天。
|
||||
|
||||
---
|
||||
|
||||
## 6. 交付前必须全跑
|
||||
|
||||
- `cd frontend && ./node_modules/.bin/tsc --noEmit` → 0 错
|
||||
- `npm run lint` → 0 error
|
||||
- `npm test` → **全量**,不是定向。失败清单与基线 `0c3c9d6b` 逐条比对,条数不得低于基线(AGENTS.md §7.3)
|
||||
- `npm run test:db` → 有 Docker 时必跑(T1 新增的是真实 Postgres 测试)
|
||||
- `npm run build` → `/` 仍 `○ Static`;首屏 gzip 与基线相差在 ±2% 内
|
||||
- 本轮不动 Python,不要求 `run_quality_gate.py`
|
||||
|
||||
进度记录写 `docs/tasks/PROGRESS-session-list-rebuild-20260921.md`,Bug 记录三条(987/988/989)写进 `docs/BUG_HISTORY.md`,用户可感知的变化写进 `CHANGELOG.md`,侧栏副标题口径变化同一提交内更新 `frontend/DESIGN.md`。
|
||||
|
||||
---
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
按此顺序让步,每让一步都要在进度记录里写明让了什么、替代证据是什么:
|
||||
|
||||
1. **无 Docker** → T1 的 `database-session-list-visibility.test.ts` 跑不了:测试文件照写照提交,在 `BLOCKED.md` 记明,并补一条不依赖 Docker 的查询构造合同测试(断言过滤条件里出现 `session_type` 限定)作为替代证据。**不得因此不写那个真实 DB 测试。**
|
||||
2. **T3 的第一问落库牵出预料外的 `?c=` / 刷新回归** → 允许把 T3 单独拆到后一轮,但 T1、T2 必须本轮交付,且拆分理由要写进进度记录和 `BLOCKED.md`。**T1 不得让步**。
|
||||
3. **首屏 gzip 超 ±2%** → 在进度记录里给出原因,不得为了指标回退功能。
|
||||
4. 任何让步都不得触碰 §4 硬红线 1、2、3。
|
||||
|
||||
---
|
||||
|
||||
## 8. 开工前置命令
|
||||
|
||||
```bash
|
||||
cd /workspace/Jyotisha
|
||||
git status -sb | head -1 # 确认没落在别人的分支上
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/session-list-rebuild-20260921 \
|
||||
.worktrees/session-list-rebuild-20260921 origin/staging
|
||||
cd .worktrees/session-list-rebuild-20260921/frontend
|
||||
npm ci # 若 node_modules 缺失
|
||||
./node_modules/.bin/tsc --noEmit # 记录基线
|
||||
npm test 2>&1 | tail -30 # 记录基线失败清单与总条数
|
||||
grep -n "" src/app/\(app\)/page.tsx | tail -1 # 记录 page.tsx 基线行数
|
||||
```
|
||||
|
||||
开工时必读:
|
||||
|
||||
- `docs/BUG_HISTORY.md` 的 BUG-553、BUG-699、BUG-704、BUG-705、BUG-926~929、BUG-933(检索关键词:`会话列表`、`messages = []`、`空会话`、`updated_at`、`draft`)
|
||||
- `BLOCKED.md` 第 120 行起那条
|
||||
- `docs/tasks/TASK-session-list-single-source-20260917.md` 与其 `PROGRESS`(本单是它的返工)
|
||||
- `AGENTS.md` §2、§3、§5、§6、§7
|
||||
|
||||
纯前端 + 一条 API route,不要求 `scripts/pre_work_check.py`(AGENTS.md §9 末段)。
|
||||
Reference in New Issue
Block a user