From 224244c01492ec498339c59943f398c1ead75ffa Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 15 Sep 2026 06:25:18 +0000 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20P0=20brief=20=E2=80=94=20opening?= =?UTF-8?q?=20a=20stored=20rectification=20session=20renames=20and=20re-so?= =?UTF-8?q?rts=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openRectificationCase 构造 merged 时无条件 title: resolveSessionTitle(…) 与 updatedAt: timestamp()。同一个对象里 pinned 和 archivedAt 是从 existing 继承 的,说明作者知道这条路径会跑在已存在的会话上,只是漏了这两个字段。 resolveSessionTitle 的日期来自 new Date(),于是 9/12 建的会话被改名成 「9月15日 · 生时校正」,同名时再追加时:分。 两种损坏持久性不同,修复时要分开:persistSession 的 update 分支写 title 但不写 updated_at(BUG-553 的正确遗产),所以标题是永久写坏服务端,排序 只坏在客户端 state、刷新自行恢复。已坏的历史标题按 created_at 修回,取不到 就退回不带日期的中性标题,不猜。 复发自 BUG-553——它的防复发「updatedAt 只由对话活动推进」只是 Bug 记录里 的一句话,没有任何测试执行,且当时只覆盖元数据 PATCH 与五条客户端路径, 漏了当时就存在的校正 open 路径。本单要求把那句话变成测试。 咨询侧三处 resolveSessionTitle 都有 messages.length === 0 && 通用标题的守卫, 不受影响,明令不得顺手改。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu --- docs/tasks/README.md | 1 + ...fication-open-retitles-session-20260915.md | 236 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 docs/tasks/TASK-rectification-open-retitles-session-20260915.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index e5e20725..0520a5bf 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -120,6 +120,7 @@ | `TASK-chat-reading-load-20260915.md` | `PROGRESS-chat-reading-load-20260915.md` | 首页主题卡下线 + 回答两层可见性 + 折叠层宽表可横滑 + 输入框 16px。内容一字不删。⚠️ BUG-630 走查路径失效 | 已验收(2 条契约测试未通过,见修复单) | `8144fca2` | | `TASK-mobile-touch-and-breakpoints-20260915.md` | `PROGRESS-mobile-touch-and-breakpoints-20260915.md` | 消息操作按钮触屏命中区 27×34、相邻仅隔 1px(BUG-695);CSS 平板上限 900px 与 `sidebarViewportForWidth` 的 1024 不一致,901–1023 是混合态(BUG-696);报告域 720/760/860 三个断点互不对齐,761–860 目录已塌、正文还是桌面(BUG-697)。含断点白名单契约测试。**串行在 chat-reading-load 之后** | 已验收 | `6c748d86`(BUG-695~697) | | `TASK-chat-reading-load-fix-20260915.md` | `PROGRESS-chat-reading-load-fix-20260915.md` | **验收修复单**:`chat-bundle-splitting-contract`(`message-markdown` 包裹数 1→2、`plainParagraphs(spoken)` 字面量已不存在)与 `chat-navigation-a11y-contract`(`window.location.assign("/login")` 4→3,建议问题入口删除带走一处)两条既有断言被本轮打红且未更新;Linux 全量 27→29。只改测试不改产品代码 | 待验收 | `codex/chat-reading-load-fix-20260915` | +| `TASK-rectification-open-retitles-session-20260915.md` | — | **P0 复发单**:点侧栏历史校正会话,`openRectificationCase` 无条件用墙钟重算 `title` 并把 `updatedAt` 盖成此刻(同一处 `pinned`/`archivedAt` 却从 `existing` 继承了),`persistSession` 把改名写回服务端 → 12 号的会话被改成「9月15日 · 生时校正」并顶到列表最前。标题**永久损坏**、排序刷新可恢复。复发自 BUG-553(其防复发只是一句话,无测试执行,且没覆盖校正 open 路径)。含历史标题按 `created_at` 修回(BUG-699) | 待领取 | `codex/rectification-open-retitles-session-20260915` | | `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal` 的 `dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` | ### 个人报告 diff --git a/docs/tasks/TASK-rectification-open-retitles-session-20260915.md b/docs/tasks/TASK-rectification-open-retitles-session-20260915.md new file mode 100644 index 00000000..a1c72728 --- /dev/null +++ b/docs/tasks/TASK-rectification-open-retitles-session-20260915.md @@ -0,0 +1,236 @@ +# TASK · P0:打开历史校正会话会把它改名成今天、并顶到列表最前(BUG-699,复发自 BUG-553) + +- 日期: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 也是「命名+排序」一起坏,用户这次的描述与当时几乎一字不差。 + +--- + +## 3. 决策记录(产品已授权) + +1. **打开已有会话不得改标题、不得改 `updatedAt`。** 只有新建才算首次命名。 +2. 已经被改坏的历史标题**要修回**,见任务 3。修不回真实日期的,宁可退回中性标题,也不要留一个错的日期。 +3. 本单只改校正 open 路径与随之而来的守卫,不重构会话列表。 + +--- + +## 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` 本地日期。 + +### 任务 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-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-698 | 已由 `TASK-settings-dialog-size-and-nav-20260915.md` 占用,本单从 **BUG-699** 起。 |