Files
Jyotisha/docs/tasks/TASK-session-list-single-source-20260917.md
T

15 KiB
Raw Blame History

任务书 · 会话列表一处数据源、服务端排序修正、空会话不入列、标题规则重定(2026-09-17)

0. 基线

  • 基线 commiteea90926origin/staging head)。串行在 TASK-rectification-session-composer-guard-20260917.md 之后:那一单改 use-session-management.ts 的回退路径,本单从它合入后的 staging 起分支,开工时以当时的 origin/staging 为准。
  • 分支:codex/session-list-single-source-20260917git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging
  • 范围:frontend/src/lib/db/local-postgres-client-core.ts(排序)、api/sessions 两个路由、lib/sidebar-data-cache.ts / hooks/use-sidebar-data.ts / hooks/use-session-management.ts / app/(secondary)/layout.tsx / components/app-sidebar.tsx / components/sidebar-session-row.tsxlib/agent-reply.ts 的标题函数、lib/home-profile.ts 的侧栏标题/副标题、lib/session-groups.ts,以及 page.tsx(只允许减行)。不动 Python、不动 Skill。T3 若选服务端过滤方案不动表结构;若产品另批清理迁移再动表并真跑 npm run test:db
  • BUG 段:BUG-926 起(上一单占 924–925;开工时核对 docs/BUG_HISTORY.md 最大号)。

1. 事故实证

产品负责人 2026-09-17 反馈:/(新建对话)、/chart/ephemeris/reports 四处的侧栏会话列表不是同一份,每换一页都重新读一次;列表内容还一直在变;排序和标题规则看不出哪个是哪个。

用产品负责人登录态只读核对 staging(GET /api/sessions?limit=50,不含身份信息):

项目 结果
返回顺序 严格按 id 降序updated_at 不单调
nextCursor 2026-09-03T07:09:23Z,<id>,按 updated_at 构造
首页 50 条里最新的 updated_at 2026-09-17 01:35Z;当天 11:12Z 新建的校正会话不在首页
首页 50 条标题分布 「新对话」28 条、「生时校正」4 条、「9月8日 · 生时校正」2 条、「9月14日 · 生时校正」2 条、其余各 1
各接口耗时 sessions 0.83 s、account 0.87 s

代码定位(按符号):

  • 服务端排序丢了第一键。 frontend/src/lib/db/local-postgres-client-core.tsorder(column, options)this.ordering = { column, ascending },只保存最后一次调用;SQL 生成处 if (this.ordering) sql += " order by …" 只有一个键。api/sessions/route.ts GET 链式调用 .order("updated_at", desc).order("id", desc),实际只按 id 排。游标 sessionCursorFilter 却按 updated_at, id 过滤,于是首页不是最近 40 条、翻页与首页重叠或漏掉、每次进入看到的集合都不同。同一缺陷还吃掉了 api/payment/packages/route.ts.order("sort_order").order("created_at"),套餐顺序变成按创建时间。
  • 两份数据源。 /page.tsx 启动 effect 自己拉 fetchSessions()(默认 40 条)+ 详情,存进 Home()sessions state,再经 visibleSessions(过滤掉空的普通会话)→ sidebarSessions 喂给侧栏;(secondary)/layout.tsxuseSidebarData() 另拉 /api/sessions?limit=40 + /api/account,进 sidebar-data-cache.ts 的 60 s 模块缓存,不做空会话过滤/app/ 里不在 (secondary) 路由组,离开 / 即卸载 Home(),回来重跑整个启动(账户、目录、列表、详情、entry-summary、今日星语);invalidateSidebarCache()/ 的每次写之后清空次级页缓存。BUG-745 修掉的是次级页之间的重挂,/ ↔ 次级页之间没有修。
  • 空会话堆积。 page.tsx 启动时 starterHomeLandingNeedsConsultation 为真(落点缺失或是校正会话)就 createSession + writeChatSession(create) 立即落库;startNewChat 也是点了就落库。用户从没发过问题的「新对话」因此在库里累积(首页 50 条里 28 条),/visibleSessions 把它们藏起来,次级页原样显示,两边列表自然不一样。
  • 标题规则。 resolveSessionTitle:普通会话在第一次回答前叫「新对话」,之后取模型标题或问题前 14 字;校正/今日节奏用 datedSessionTitle 生成「M月D日 · 生时校正」;同名靠 uniquifySessionTitle 追加「HH:MM」(新建时的墙钟,不是会话时间)。旧数据还有无日期的「生时校正」。侧栏行只有标题一行,/ 的副标题只在他人盘时显示,次级页根本没有副标题字段。结果是一列「新对话 / 9月14日 · 生时校正 / 9月14日 · 生时校正 02:14」。
  • 服务端 updated_at 口径(BUG-553:只由对话活动推进,元数据 PATCH 不写)仍在,不是本单问题;客户端 sortSessions 置顶 + updatedAt 倒序也正确,只是服务端给的集合本身不对。

Bug 历史检索:BUG-553(列表只按置顶排)、BUG-557(标题守卫 0 行)、BUG-699(打开旧校正会话改今天标题)、BUG-744~746(侧栏统一、次级页重挂、折叠状态)防复发措施均在,本单四条都是新问题;BUG-745 的「每跳一次重拉列表」在 / ↔ 次级页方向属未覆盖,关联但非复发。

2. 根因

  1. 本地 PostgreSQL 兼容层的 order() 不支持多键,服务端列表从来没有按最近活动排过;游标分页因此与排序不一致。
  2. 会话列表没有一个跨路由的所有者:Home()(secondary) 各拉各的,/ 又是每次进入从零启动。
  3. 会话在用户还没说一句话时就落库,而两处列表对空会话的处理不一致。
  4. 标题只承载「叫什么」,不承载「哪一类、哪一天、谁的盘」,同名去重又用墙钟时间。

3. 决策记录

  • 产品负责人 2026-09-17 授权:列表必须共用一份;排序与标题规则重构。以下具体口径由 Claude 定,产品负责人如不同意在验收时改:
    • 排序:置顶在前;组内按 updated_at 倒序;updated_at 仍只由对话活动推进(BUG-553 不变)。服务端与客户端同一口径。
    • 哪些会话入列:只列有内容的会话——普通会话至少一条消息;校正会话一律入列(它一打开就有开场轮)。当前正在用的空会话只在 / 本地显示,不入服务端列表。
    • 标题:普通会话 = 模型标题,否则第一问前 14 字;第一次回答落库前显示「新对话」但不入列。校正会话 = 「生时校正 · M月D日」;今日节奏 = 「今日节奏 · M月D日」——类别在前,日期在后,扫一眼能分类。同日多条不再加墙钟「HH:MM」,改为副标题区分。
    • 副标题(两处侧栏都显示一行小字):M月D日 HH:MM(会话创建时间,Asia/Shanghai+「 · <盘主名>」(仅他人盘)。校正会话再补状态词:进行中 / 已交付 / 已结束(从 entry-summary 或列表字段取;取不到就不显示,不得猜)。
    • 存量数据:旧标题不批量改(BUG-699 已做过一次校正会话标题修复,且手改标题不能动);只对未来新建生效。空会话不删,只是不入列;若产品负责人另批清理,另开迁移单。
  • 不推翻 BUG-553 / BUG-699 / BUG-745 的任何防复发措施。
  • page.tsx 不得增长;Home() 拥有的乐观更新与回滚层保留,只是数据源换成共享 store。

4. 硬红线

  1. 兼容层 order() 改多键后,所有既有单键调用行为不变;有 SQL 文本级测试。
  2. 服务端列表排序键与 sessionCursorFilter 的键必须一致(updated_at desc, id desc),有测试锁住。
  3. 元数据 PATCH 仍不写 updated_atBUG-553);打开已有会话不改 title / updatedAtBUG-699session-open-preserves-identity.test.ts 必须保持绿)。
  4. 次级页不得再挂第二份 SidebarProviderBUG-745);侧栏源码不得出现 window.location
  5. 既有测试断言不得静默弱化;改断言写「原值 / 新值 / 原因」三栏。测试总数不低于开工时。
  6. 无 Docker 时 npm run test:db 失败照基线记录,不得写成通过。

5. 任务分解

T1 兼容层多键排序 + 列表接口排序契约(BUG-926)

  • local-postgres-client-core.tsordering 改为数组,order() 追加;SQL 生成 order by a desc, b desc
  • api/sessions/route.ts 两处查询(分页与置顶)保持 .order("updated_at", desc).order("id", desc)api/payment/packages/route.ts 不改代码,验证顺序恢复为 sort_order, created_at
  • 验收:
    • frontend/tests/local-postgres-query-value.test.ts(或新建 local-postgres-order.test.ts):两次 order() 生成的 SQL 含 order by "updated_at" desc, "id" desc;单次 order() 输出不变。
    • frontend/tests/session-cursor.test.tschat-session-authority.test.ts 加断言:列表路由源码的排序键序列与 sessionCursorFilter 的键一致。
    • 有 Docker 时 npm run test:db 加一条:插入三条不同 updated_at 的会话,GET /api/sessions?limit=2 返回最新两条且 nextCursor 翻页拿到第三条、无重叠。无 Docker 写 BLOCKED.md

T2 一份会话列表 store/ 与次级页共用(BUG-927

  • 新建 frontend/src/lib/session-list-store.ts:模块级 storeuseSyncExternalStore),按账户 id 分区,保存列表行、游标、账户摘要、fetchedAt;提供 readSessionList()subscribehydrateFromServer()applyLocalWrite(op)create / rename / pin / archive / delete / activityBump)与 revalidate()(后台重拉,先渲染现有行)。sidebar-data-cache.ts 并入或删除,不留两份。
  • Home() 启动:若 store 已有本账户列表,先用它渲染侧栏与落点选择,再后台 revalidate();没有才拉。Home() 现有的 sessions state 继续作为详情(消息)的所有者,但列表行的来源与写操作统一通过 store;updateSession / persistSession 成功后同步 applyLocalWrite
  • (secondary)/layout.tsxuseSidebarData() 改读同一 store;不再自己发 /api/sessions,除非 store 为空或过期(60 s 保留)。
  • / ↔ 次级页往返不得再触发 /api/sessions/api/account60 s 内);invalidateSidebarCache() 改为 applyLocalWrite,不清空整份。
  • 验收:
    • sidebar-data-cache.test.ts 改为 store 测试:写入后两处读同一份;rename 后次级页立刻是新标题;60 s 内 revalidate() 不发请求;账户切换隔离。
    • sidebar-contract.test.ts / home-bootstrap-reveal.test.tsHome() 有缓存时启动不发 /api/sessionsmock fetch 计数为 0),有缓存也不阻塞揭幕。
    • 真机清单(docs/testing/):DevTools Network 观察 //chart/ephemeris/reports/ 全程 /api/sessions 只出现一次;改名后各页标题一致。

T3 空会话不入列,且延迟落库(BUG-928)

  • 服务端:api/sessions GET 排除 session_type = 'consultation' AND messages = '[]'(保留校正会话);置顶查询同样处理。GET /api/sessions/<id> 不变(详情仍可读)。
  • 客户端:startNewChat 与启动时的落点会话改为本地创建、不立即落库ChatSessionpersisted: booleansend() 在撤回窗口结束、POST /api/consult 之前若 !persistedwriteChatSession(create)(失败则回滚并提示,沿用 startNewChat 现有回滚文案)。校正会话仍由 /cases/open 服务端创建,不受影响。
  • visibleSessions 的过滤保留(当前空会话只在 / 本地可见),次级页因服务端已过滤而自然一致。
  • 让步:若延迟落库牵动 ?c= 深链、刷新恢复(pendingConsultationStorageKey)过多,可只做服务端过滤 + 启动时复用已有空会话而不再新建,把延迟落库写进 BLOCKED.md
  • 验收:
    • chat-session-authority.test.ts:列表路由过滤条件存在;详情路由不过滤。
    • 新测试:连点三次「新建对话」不发 POST /api/sessions;发出第一问前恰好一次 POST /api/sessions?c=<本地未落库 id> 刷新后不报「会话不存在」而是回到落点。
    • 有 Dockertest:db 加一条空会话不出现在列表。

T4 标题与副标题规则重定(BUG-929)

  • agent-reply.tsdatedSessionTitle 改为「<类别> · M月D日」;uniquifySessionTitle 删除(同名允许,靠副标题区分);isGenericSessionTitle 的前缀正则同步改成类别在前,并兼容旧「M月D日 · 生时校正」格式(旧标题仍判为 generic,行为不变)。
  • 列表接口增加 created_at(表里已有;只是没选出来)。toSidebarSessionsidebarSessions 共用一个 lib/session-sidebar-row.tstitlesubtitle = 「M月D日 HH:MM」+「 · 盘主」、校正状态词。sidebar-session-row.tsx 副标题已支持,无需新组件;两处侧栏必须走同一个映射函数。
  • 分组(今天 / 昨天 / 最近 7 天 / 30 天 / 更早)保留,按 updated_at
  • frontend/DESIGN.md Sidebar shell 一节写明标题与副标题规则;frontend/docs/VOICE.md 对照「生时校正 · 」「今日节奏 · 」措辞。
  • 验收:agent-reply.test.ts 更新(写三栏);新测试锁映射函数:同一行输入在 / 与次级页产出相同 title/subtitlesession-open-preserves-identity.test.ts 保持绿。

T5 记录

  • docs/BUG_HISTORY.md 新增 BUG-926929926 关联 packages 路由;927 关联 BUG-745928 关联 BUG-553929 关联 BUG-699);CHANGELOG.mddocs/tasks/PROGRESS-session-list-single-source-20260917.mddocs/tasks/README.md 状态板行;BLOCKED.md(无 Docker 项)。

6. 让步顺序

T1 > T2 > T3(服务端过滤部分)> T4 > T3(延迟落库部分)。T1 一项独立可发,若其余来不及,T1 单独推 staging 也值得。

7. 开工前置命令

git fetch origin --prune
git log -1 --format='%h %s' origin/staging   # 确认上一单已合入
git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging
cd .worktrees/session-list-single-source-20260917/frontend
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20   # 记下基线失败清单与测试总数
grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1

8. 验收口径

tsc --noEmit 0 错;npm run lint 0 errornpm test 失败清单与基线逐条一致、新增测试全绿、总数不降;next build/ 仍 Static;首屏 gzip ±2%page.tsx 行数 ≤ 开工时;有 Docker 则 npm run test:db 通过,无 Docker 写 BLOCKED.md。部署后用 DevTools 复核 T2 的「一次 /api/sessions」与 GET /api/sessionsupdated_at 单调递减。