From ac224cef6a3021e1b808601124ad0e7cbf05b3f5 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 26 Sep 2026 10:15:30 +0800 Subject: [PATCH] docs(tasks): briefs for rectification duplicate question, latency, scroll anchor fixes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8 --- docs/tasks/README.md | 3 + ...ASK-rectification-dup-question-20260926.md | 53 ++++++++++++++++ .../TASK-rectification-latency-20260926.md | 61 +++++++++++++++++++ .../TASK-scroll-anchor-hook-fixes-20260926.md | 29 +++++++++ 4 files changed, 146 insertions(+) create mode 100644 docs/tasks/TASK-rectification-dup-question-20260926.md create mode 100644 docs/tasks/TASK-rectification-latency-20260926.md create mode 100644 docs/tasks/TASK-scroll-anchor-hook-fixes-20260926.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index f34808b8..ddd8c3f3 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -35,6 +35,8 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | +| `TASK-rectification-dup-question-20260926.md` | — | **同一轮问题出现两次(BUG-1045,复发自 BUG-585,BUG-969 拼回题干、去重只在刷新路径)+ 同一道选择题连画两张(BUG-1046:提交失败不回滚本地已答 + 兜底问题块条件过宽;H2 漏收回合)**。先于 latency 单 | 待领取 | — | +| `TASK-rectification-latency-20260926.md` | — | **每轮等待过长(BUG-1047)**:一轮打字回答串行 5 次开思考的模型调用,分类在开流前且无超时。产品定:收尾两步不动、分类保持思考只加 10 秒超时、发出后立即出确定性进度句并按阶段更新;补埋点;服务端无口吻优化须 A/B 逐位一致。排在 dup-question 之后 | 待领取 | — | | `TASK-rectification-session-title-result-20260922.md` | `PROGRESS-rectification-session-title-result-20260922.md` | **校正会话标题改写结果(BUG-1001)**:BUG-988 把副标题改成最后活动时间后,标题里的日期成了重复;而 BUG-929 删掉 `uniquifySessionTitle` 之后,`resolveSessionTitle` 对校正入口只返回 `生时校正 · M月D日`,**同一天多条标题完全相同**(真机截图:9/17 三条同名、9/16 两条同名、9/14 一天 5 条);旧标题的钟点后缀是创建时间、副标题是最后活动时间,同一行两个对不上的时间。**产品 09-22 拍板**:D1 标题改为承载结果——有 `accepted_time`/`confirmed_time` 写 `生时校正 · HH:MM`,否则写 `candidate_range` 的 `生时校正 · HH:MM–HH:MM`(推翻 BUG-929 的日期口径,但「不得再加墙钟去重后缀」保留);D2 存量批量重算(推翻 BUG-929「旧标题不批量改」,先例 `20260916020000_rectification_session_title_repair.sql`);D3 今日节奏保留日期(我判定的例外);D4 只在结果变化时改 title,`open` 路径不动(对 BUG-699 边界的有限扩展)。**红线**:任何写标题的路径都不得 bump `updated_at`(否则 13 条历史会话集体跳顶、毁掉 BUG-988);标题算式必须只有一处实现(一个 SQL 函数,触发器与回填共用)——BUG-987/992 都栽在「两层规则各写一份」。T4 跨层对断不得让步。已 grep 出真正受影响的只有 7 个文件,Python 侧只查裸字符串、无需改。BUG 段 1001 起 | 已验收(Claude 09-25:单一 SQL 函数、不动 updated_at、手改名不覆盖、回填幂等) | `88fe67df`/`a94a1d67`(已部署) | | `TASK-rectification-convergence-20260830.md` | `PROGRESS-rectification-convergence-20260830.md` | 收敛重构 v2 | 已验收 | `3a4396a4`、`86ba17ee` | | `TASK-rectification-decision-authority-20260831.md` | `PROGRESS-rectification-decision-authority-20260831.md` | 决策权威归一与停止语义 | 已合入 | `9f011194` | @@ -119,6 +121,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | +| `TASK-scroll-anchor-hook-fixes-20260926.md` | — | **滚动锚两处老问题**:直接打开已有会话时监听未挂上(BUG-1043)、校正长回答钉顶后因 96px 阈值被拉到底(BUG-1044)。排在 BUG-1042 合入后 | 待领取 | — | | `TASK-latest-turn-actions-gap-20260926.md` | — | **最后一轮正文与点赞 / 踩之间空大半屏**:BUG-930 钉顶留白(`min-height: 视口 − 本轮开头`)加在 `.message-assistant` 上,把兄弟节点 `.message-actions` 推到留白之后;改为加在整轮外层,按钮紧贴正文、空白落在后面;不动滚动 hook | 待领取 | — | | `TASK-starter-home-polish-20260926.md` | `PROGRESS-starter-home-polish-20260926.md` | **首页开场小字与图标**:今日趋势(每日模型生成,非写死)移到问候下方副行;入口下方提示只在有未完成校正或非本人时出现,删两句固定文案并修正已校正仍显示首次文案的分支;今日星语图标 MoonStar、点数图标 Coins。排在 BUG-1038、1040 之后。分支误判实为入口摘要解析读错键名(BUG-1041) | 已验收(Claude 09-26 直接执行:子代理实现并查出 BUG-1041 入口摘要驼峰/下划线字段不一致;Claude 改恢复提示文案为「可以在历史对话里接着做」;独立复验 tsc/lint 0、全量 3960 条失败名单与基线逐条一致、四路由 ○、gzip 不变) | `f04da103`(已部署 `62d4c9c4`,health 一致) | | `TASK-home-warm-return-20260926.md` | — | **从其他页面回首页不再放加载动画**:首页按组件挂载计揭幕,客户端返回会重跑模型列表 / 后台状态 / 入口摘要 / 今日星语(3–5 次往返 + 最多 4 秒)。产品定:每次打开网页只揭幕一次;模块级暖快照按账户隔离、只存内存,暖返回首帧即就绪、后台静默刷新;缺项回落冷启动,不许半揭幕。**排在 BUG-1038 合入后**。BUG-1040 | 已验收(Claude 09-26 直接执行:子代理实现,Claude 独立复验 tsc/lint 0、全量 3953 条失败名单与基线逐条一致、四路由 ○、gzip 不变;CDP 实测回首页 5.5 s → ~20 ms、无加载环) | `638b6a60`(已部署 `7475eb6e`,health 一致) | diff --git a/docs/tasks/TASK-rectification-dup-question-20260926.md b/docs/tasks/TASK-rectification-dup-question-20260926.md new file mode 100644 index 00000000..38d72786 --- /dev/null +++ b/docs/tasks/TASK-rectification-dup-question-20260926.md @@ -0,0 +1,53 @@ +# TASK · 生时校正:同一轮问题出现两次 + 同一道选择题连画两张(2026-09-26) + +## 基线 + +- `origin/staging` 当前 head(`a57c4310` 代码;BUG-1042 修复单合入后以新 head 为准)。分支 `codex/rectification-dup-question-20260926`。 +- 与 `TASK-rectification-latency-20260926`(改 route / agent-run / 校正面活动状态)**冲突**:本单先做,那单后做。与 `TASK-scroll-anchor-hook-fixes-20260926` 不交叉。 + +## 事故实证(产品 09-26 真机,staging) + +**A · 同一轮里问题出现两次**:用户打字回答后,助手这一轮先是正文「已记录,范围没变;14:58–14:59 领先,14:40–14:43 落后。」「2024 年 3 月前后,有没有收入明显变化、大笔支出或欠债?」,下面又是加粗的同一句问题块和选项。刷新后只剩一遍。 + +- `frontend/src/lib/rectification-agentic/v9/answer-choice.ts` `persistApplied`:下一问是选择题时 `hostNarration = composeCollectSpokenAssistantText(body, choiceStem)`,把题干拼进 `assistant_message` 并作为 `applied.narration` 返回;`persistCollectDenialTurn` 同样。 +- `frontend/src/app/api/rectification/agent/route.ts`:打字回答命中待答选择题(`action === "message"` + `choice` 分支)→ `applyRectificationChoice` → `completedMessageResponse(applied.narration)` 整段作为一个 `answer.delta` 下发;不经模型 runner,`agent-run.ts` 里 BUG-585 的 `stripQuestionSentences(...)` 不执行。 +- `rectification-agentic-chat.tsx` `send` 结算后保留原始流式正文;`loadCaseSnapshot` → `frontend/src/lib/rectification-snapshot-messages.ts` `mergeTurnQuestions` 只拷 `question` / `candidateOffer`,**不替换 `text`**。`rectification-message-entry.tsx` 先渲染 `message.text` 再渲染 `question.prompt` → 两遍。去重只在 GET 组装的 `attachQuestionsToTurns`(`turn-question.ts`)里,所以刷新后消失。 +- 排查已在 tsx 里复现:合并快照后正文仍含题干;`stripQuestionSentences` 对同一文本能正确去掉。 +- **复发**:BUG-585(修复 `9d1c08ca`,防复发"不得恢复把题干拼进 assistant_message")被 **BUG-969 第③项**(`824ecff0`,2026-09-18)带回:为防卡片加载失败时看不到题而拼回题干,声称"挂卡后去重",但测试(`account-dialog-inert-20260918.test.ts`)只覆盖刷新路径,并把 `rectification-answer-choice.test.ts` 断言改成期望题干在正文里。 + +**B · 同一道选择题连画两张**:用户在第 N 轮点选 **B 发生过但程度较弱**(高亮),其下是操作按钮、「目前范围 14:35-15:05,还在核对」,再下面又出现完全相同的题干和四个选项(未选中)。 + +- 第二张不是新一轮助手回答,而是独立的"持久化问题块":`rectification-agentic-chat.tsx` 中 `questionGap === "persisted_question"` 时渲染 `currentQuestion.prompt` + 新的 `RectificationChoiceCard`(`selectedKey=""`)。条件是 `rectificationQuestionGapState` 判定"没有任何消息挂着与 `currentQuestion.focus_id` 匹配的活题,但快照里仍有持久化问题"。同类:BUG-675 / 678(「目前范围」下挂裸题);BUG-678 意图"只在没有助手消息时兜底",但闸门没强制。 +- 两个假设(需真实会话区分): + - **H1(更可能)**:`submitStructuredChoice` 先 `markQuestionAnswered`(本地标 B)再 POST `/api/rectification/agent`(`action: "answer_choice"`);`!response.ok` / `catch` 只移除占位、`choiceNonce++`、`setError`,**不撤回已答标记、不重载快照** → 活题判定落空 → 兜底块再画一张。服务端可能的错误:`stale_question` / `revision_conflict` / `stale_probe` / 500。 + - **H2**:POST 成功但 `nextInterviewPersisted` 为假且 `nextAction` 为 `ask_candidate_discriminator` 的 `willContinue` 分支只跑 `mergeTurnQuestions`、没跑 `appendUnseenAssistantTurns`(BUG-685 在 `requestTieBreak` 修过同型缺口),承载下一问的那轮没进客户端,兜底块替它画了出来;若题目完全相同,还说明选题重复了(BUG-540 / 559 / 592 守卫仍在代码里,需确认)。 + +## 根因 + +A:服务端为兜底把题干拼进正文,客户端在实时路径上没有"挂卡即去重"。B:选择题提交失败不回滚本地状态 + 兜底问题块的出现条件过宽(H2 另有漏收回合)。 + +## 决策记录 + +- D1 保留 BUG-969 的兜底意图(卡片没挂上时用户仍能在正文看到题),但**卡片一挂上就去掉正文里的同一句**:`mergeTurnQuestions` 在快照回合带问题时,用服务端已去重的 `turn.text` 覆盖,或对 `message.text` 执行 `stripQuestionSentences(text, question.prompt)`;实时流里若同一轮已收到问题卡,也立即去重。 +- D2 选择题提交失败:撤回本地已答标记(题目回到 active、无 `answer_option`)、重载快照、只保留**一张**可点的卡 + 错误提示「这次没提交上,请再点一次。」(对照 VOICE)。 +- D3 兜底问题块只在**没有任何助手消息**时渲染;已有助手消息时把问题挂到最新助手消息上(BUG-678 原意),且同 `focus_id` 的题永远只画一张。 +- D4 H2 分支:`willContinue` 与 `send()` 结算后的快照合并同时执行 `appendUnseenAssistantTurns`(同 BUG-685)。若复现确认服务端真出了重复题,另开 BUG 关联 BUG-540 / 592,不在本单改选题。 + +## 硬红线 + +1. 回归测试必须走**实时路径**:流式收到"确认句 + 题干"→ 合并快照 → 断言题干在正文与问题块合计只出现一次;打字回答命中选择题、采集拒答两条路径各一条。 +2. 提交失败的回滚测试:模拟 409 `stale_question` 与网络错误,断言页面只有一张可点卡 + 错误提示,已答标记被撤回。 +3. 不改服务端选题与打分;不改 BUG-969 的服务端兜底文本(只改客户端去重)——若必须改服务端,写明理由。 +4. 全量测试与基线逐条一致、新增 0;改动断言写三栏(含把 BUG-969 改成"期望题干在正文"的那条,改回并说明);真实浏览器截图(CDP 假数据)验证 A、B 两个场景。 +5. 不改 `useConversationScrollAnchor`(另一单在改)。 + +## 任务分解 + +- T1 实时去重(D1)+ 测试。 +- T2 提交失败回滚(D2)+ 兜底块闸门(D3)+ 测试。 +- T3 H2 漏收回合(D4)+ 测试。 +- T4 记录:BUG-1045(A,**复发自 BUG-585**,写明 BUG-969 如何绕过防复发、为何测试没拦住);BUG-1046(B,关联 BUG-675 / 678 / 685);PROGRESS;`docs/testing/` 真机清单(打字回答后问题只出现一遍;选择题提交失败时只有一张卡 + 提示;刷新前后一致)。 + +## BUG 编号 + +写单时最大 BUG-1044(1042–1044 由按钮间距单预留);本单 **BUG-1045 / 1046**,开工时核对。 diff --git a/docs/tasks/TASK-rectification-latency-20260926.md b/docs/tasks/TASK-rectification-latency-20260926.md new file mode 100644 index 00000000..8b8d3ce8 --- /dev/null +++ b/docs/tasks/TASK-rectification-latency-20260926.md @@ -0,0 +1,61 @@ +# TASK · 生时校正每轮等待过长:埋点 + 分类超时 + 发出后立即反馈(2026-09-26) + +## 基线 + +- `origin/staging` 当前 head。分支 `codex/rectification-latency-20260926`。 +- **串行**:排在 `TASK-rectification-dup-question-20260926`(同样改校正面组件)之后。 + +## 事故实证(产品 09-26 真机) + +打字回答后「正在分析 / 正在处理…」要等很久才出正文。排查结论(`origin/staging` a57c4310,本地实测 + 代码阅读): + +- 一轮打字回答串行 **5 次模型调用**,全部开思考: + 1. 开流前意图分类 `classifyTurnIntentWithRetry`(`v9/turn-intent-classifier.ts`;`agent/route.ts` 约 L453 / 534 / 643 / 751 调用),用会话模型、供应商默认开 thinking,**每次尝试无超时**、最多 2 次,且发生在 `new ReadableStream` 之前 → 用户看到的是一段没有任何字节的空等;L643 分支继续时 L751 还会再分类一次。 + 2–5. Agent 四步(`v9/agent-run.ts` `streamAttempt`,`agentGenerationSettings(..., { thinking: "enabled", thinkingTokens: 8192 })`):强制第 0 步 `rectification-read-case`(路由已读过档案)→ `record-evidence-batch`(含引擎重算)→ `set-focus`(改写服务端定好的下一问)→ 最终正文模板句。推理内容不下发(`stream-mapping.ts`),界面只显示「正在分析…」。 +- 引擎:09-15 性能审计四单(BUG-721~726)**已全部落地**;本地实测 20 个候选 0.4–0.5 s、61 个候选 2.3–2.5 s、带 `refresh_probes` 5.8–6.1 s;cProfile 热点是 `build_refinement_packet`(57–73%),refresh 分支探针算两遍;交付阶段 `vedastro-validate` 串行调外网(≤12 s)。 +- 收尾 `persistNextInterviewIfIdle` 在 `agent-run.ts` 与 route `finalizeSuccessfulTurnExit` 各调一次,第二次多为空转但会重读档案。 +- `RectificationRunDiagnostic` 的 `inputTokens` / `reasoningTokens` 恒为 null,`stepCount` 实际是"用过几种工具",分类耗时只在失败时记录 → 线上无法核实各段耗时。 +- 估计单轮 25–90 s,模型占 85–95%;evidence_not_written 重跑时翻倍。 + +## 根因 + +串行多步 + 每步深度思考 + 开流前无超时空等 + 无进度反馈。 + +## 决策记录(产品 2026-09-26) + +- D1 **不改**每轮最后两步(set-focus 改写与最终正文仍由模型生成,口吻不变)。 +- D2 **分类保持深度思考与会话模型**(维持 09-15 决定),只给每次尝试加 **10 秒超时**;超时按现有失败路径处理(重试一次后走既有兜底),不得换模型、不得关思考。 +- D3 **发出后立即反馈**:用户发出后 ≤ 300 ms 内在活动状态处出现确定性进度句,随后按阶段更新,替代一直不变的「正在分析 / 正在处理…」: + - 发出即:「收到,正在对照你的档案…」 + - 记录经历开始:「正在记下这件事…」 + - 引擎重算:「正在重新对照盘面…」 + - 准备下一问:「正在准备下一个问题…」 + - 阶段信号来自现有 `persistCommittedPhase` / 工具调用事件,不新增模型调用。这些句子属于"流式生成中",不违反"揭幕后不得出现加载态";进度句不写入 `assistant_message`。 + - 为此把分类移到流内:先建流、立刻推第一句进度,再做分类。 +- D4 **补埋点**:每步开始 / 结束时间、每步推理 token(供应商能给则记)、分类耗时(成功也记)、引擎每次调用耗时,写入 `RectificationRunDiagnostic`(不含用户资料与模型原文,符合隐私红线)。 +- D5 **无口吻影响的服务端优化**(输出必须逐位不变,A/B 证明): + - refresh 分支里 `build_refinement_packet` 探针只算一遍; + - `persistNextInterviewIfIdle` 第二次调用在第一次已完成时跳过; + - 部署未配置时 `readV9EngineScoringIdentity` 的 `/v5/versions` 结果在进程内按版本号缓存(短 TTL)。 +- D6 不在本单:去掉强制 read-case 第 0 步(需改 Skill 原文与回执不变量,另议);vedastro-validate 改后台(交付语义,另议)。 + +## 硬红线 + +1. 不换分类模型、不关分类思考、不改 Agent 各步是否开思考;不改 Skill 文本与 VOICE 口吻(进度句除外,需进 VOICE)。 +2. D5 每项:同一虚构输入改前改后结果逐位一致(Python 用同机 A/B,不写死跨机浮点哈希,见 BUG-985)。 +3. 分类超时测试:模拟挂起的分类调用,10 秒后进入既有失败路径,不无限等待。 +4. 进度句测试:发出后第一帧(或 ≤ 300 ms)出现第一句;阶段事件驱动更新;结算后进度句消失,不进入持久化正文。 +5. `jyotish_api_server.py` 不增长;全量前端测试与基线逐条一致、新增 0;Python 快速门与定向测试通过;改动断言三栏。 +6. 真实浏览器(CDP 假数据 + 人为延迟)截图:发出后立刻出现进度句并随阶段变化。 + +## 任务分解 + +- T1 埋点(D4)。 +- T2 分类移入流内 + 10 秒超时(D2、D3 前半)。 +- T3 阶段进度句(D3)+ VOICE / DESIGN。 +- T4 服务端无口吻优化(D5)+ A/B 证明。 +- T5 记录:BUG-1047(每轮等待过长 / 开流前无反馈),关联 BUG-721~726、BUG-722;CHANGELOG;PROGRESS(各段耗时改前 / 改后估计,部署后用新埋点复核);`docs/testing/` 真机清单(打字回答后立刻出现「收到,正在对照你的档案…」并逐段变化;总耗时记录)。 + +## BUG 编号 + +本单 **BUG-1047**(1045 / 1046 由重复问题单预留),开工时核对。 diff --git a/docs/tasks/TASK-scroll-anchor-hook-fixes-20260926.md b/docs/tasks/TASK-scroll-anchor-hook-fixes-20260926.md new file mode 100644 index 00000000..ffd5e244 --- /dev/null +++ b/docs/tasks/TASK-scroll-anchor-hook-fixes-20260926.md @@ -0,0 +1,29 @@ +# TASK · 滚动锚两处老问题:直接打开会话时监听未挂上 + 校正长回答被拉到底(2026-09-26) + +## 基线 + +- BUG-1042 修复单(`080ea5ca`,按钮间距)合入后的 `origin/staging`。分支 `codex/scroll-anchor-hook-fixes-20260926`。 +- 只改 `frontend/src/hooks/use-conversation-scroll-anchor.ts` 与其调用处;与 `TASK-rectification-dup-question-20260926` 不交叉,可并行。 + +## 事故实证(BUG-1042 执行方用 CDP 实测发现) + +- **BUG-1043(普通咨询)**:页面直接打开一个已有消息的会话(刷新、`?c=` 深链、启动落到最近会话)时,hook 的 scroll 监听与 `ResizeObserver` 从未挂上(插桩计数 0)。表现:短回答也显示「跳到最新」,滚动不更新它,点了也不跟随。先开 `/?new=1` 再点进同一会话则正常。疑似原因:加载屏期间容器为 null,effect 提前返回;揭幕后 `active` / `resetKey` 未变,effect 不重跑。 +- **BUG-1044(生时校正长回答)**:钉顶后视口距底部 94 px,落在 hook 的 96 px「已回到底部」阈值内,`measure()` 把它当作读者回到底部 → 释放 `holdUnpin`、`anchored = true` → 长回答被拉到底。普通咨询停在约 198 px,不受影响。改前改后行为一致(非 BUG-1042 引入)。 + +## 决策记录 + +- D1 BUG-1043:监听挂载必须在滚动容器真正出现后发生(容器 ref 变化时重新挂载,或以 callback ref 驱动),不依赖 `active` / `resetKey` 变化。 +- D2 BUG-1044:钉顶后在用户**主动**滚动之前,不得因"距底部小于阈值"自动恢复贴底;判断"回到底部"必须来自用户滚动事件,而不是钉顶 / 留白造成的几何位置。 +- D3 BUG-930 / 931 / 932 / 1042 的全部行为与测试不得弱化。 + +## 硬红线 + +1. 真实 React 生命周期测试:以"加载屏 → 揭幕"的方式挂载已有消息的会话,断言监听已挂上、短回答不显示「跳到最新」;校正面长回答钉顶后不被拉到底,用户手动滚到底后恢复跟随。 +2. 真实浏览器(CDP 假数据)复测两个场景并截图。 +3. 全量测试与基线逐条一致、新增 0;改动断言三栏;更新 DESIGN 对应说明。 + +## 任务分解 + +- T1 BUG-1043(D1)+ 测试。 +- T2 BUG-1044(D2)+ 测试。 +- T3 记录:更新 BUG-1043 / 1044(BUG-1042 单已建号,状态 investigating → 修复后按部署结果改);PROGRESS;`docs/testing/` 清单。